Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remove pyrfc3339 and change to datetime.datetime.fromisoformat… #1247

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
39 changes: 21 additions & 18 deletions juju/application.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Copyright 2023 Canonical Ltd.
# Licensed under the Apache V2, see LICENCE file for details.
from __future__ import annotations

import asyncio
import hashlib
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Union

from typing_extensions import deprecated

Expand Down Expand Up @@ -61,7 +61,7 @@ def min_units(self) -> int:
return self.safe_data["min-units"]

@property
def constraints(self) -> Dict[str, Union[str, int, bool]]:
def constraints(self) -> dict[str, str | int | bool]:
return self.safe_data["constraints"]

@property
Expand Down Expand Up @@ -112,7 +112,7 @@ def subordinate_units(self):
return [u for u in self.units if u.is_subordinate]

@property
def relations(self) -> List[Relation]:
def relations(self) -> list[Relation]:
return [rel for rel in self.model.relations if rel.matches(self.name)]

def related_applications(self, endpoint_name=None):
Expand Down Expand Up @@ -579,7 +579,7 @@ def attach_resource(self, resource_name, file_name, file_obj):
data = file_obj.read()

headers["Content-Type"] = "application/octet-stream"
headers["Content-Length"] = len(data)
headers["Content-Length"] = str(len(data))
data_bytes = data if isinstance(data, bytes) else bytes(data, "utf-8")
headers["Content-Sha384"] = hashlib.sha384(data_bytes).hexdigest()

Expand All @@ -589,7 +589,7 @@ def attach_resource(self, resource_name, file_name, file_obj):

headers["Content-Disposition"] = f'form-data; filename="{file_name}"'
headers["Accept-Encoding"] = "gzip"
headers["Bakery-Protocol-Version"] = 3
headers["Bakery-Protocol-Version"] = "3"
headers["Connection"] = "close"

conn.request("PUT", url, data, headers)
Expand Down Expand Up @@ -638,14 +638,15 @@ async def run(self, command, timeout=None):
)

@property
def charm_name(self):
def charm_name(self) -> str:
"""Get the charm name of this application

:return str: The name of the charm
"""
return URL.parse(self.charm_url).name
return URL.parse(self.safe_data["charm-url"]).name

@property
@deprecated("Application.charm_url is deprecated and will be removed in v4")
def charm_url(self):
"""Get the charm url for this application

Expand Down Expand Up @@ -733,14 +734,14 @@ async def set_constraints(self, constraints):

async def refresh(
self,
channel: Optional[str] = None,
channel: str | None = None,
force: bool = False,
force_series: bool = False,
force_units: bool = False,
path: Optional[Union[Path, str]] = None,
resources: Optional[Dict[str, str]] = None,
revision: Optional[int] = None,
switch: Optional[str] = None,
path: Path | str | None = None,
resources: dict[str, str] | None = None,
revision: int | None = None,
switch: str | None = None,
):
"""Refresh the charm for this application.

Expand Down Expand Up @@ -841,15 +842,17 @@ async def refresh(
# need to process the given resources, as they can be
# paths or revisions
_arg_res_filenames = {}
_arg_res_revisions = {}
_arg_res_revisions: dict[str, str] = {}
for res, filename_or_rev in arg_resources.items():
if isinstance(filename_or_rev, int):
_arg_res_revisions[res] = filename_or_rev
else:
_arg_res_filenames[res] = filename_or_rev

# Get the existing resources from the ResourcesFacade
request_data = [client.Entity(self.tag)]
request_data: list[client.Entity | client.CharmResource] = [
client.Entity(self.tag)
]
resources_facade = client.ResourcesFacade.from_connection(self.connection)
response = await resources_facade.ListResources(entities=request_data)
existing_resources = {
Expand Down Expand Up @@ -930,8 +933,8 @@ async def local_refresh(
force: bool,
force_series: bool,
force_units: bool,
path: Union[Path, str],
resources: Optional[Dict[str, str]],
path: Path | str,
resources: dict[str, str] | None,
):
"""Refresh the charm for this application with a local charm.

Expand Down Expand Up @@ -1012,8 +1015,8 @@ async def get_metrics(self):

def _refresh_origin(
current_origin: client.CharmOrigin,
channel: Optional[str] = None,
revision: Optional[int] = None,
channel: str | None = None,
revision: int | None = None,
) -> client.CharmOrigin:
chan = None if channel is None else Channel.parse(channel).normalize()

Expand Down
7 changes: 5 additions & 2 deletions juju/client/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import packaging.version
import typing_inspect
from typing_extensions import TypeAlias
from typing_extensions import Self, TypeAlias

from . import codegen

Expand Down Expand Up @@ -661,6 +661,9 @@ def default(self, obj: _RichJson) -> _Json:


class Type:
_toSchema: dict[str, str]
_toPy: dict[str, str]

def connect(self, connection):
self.connection = connection

Expand All @@ -678,7 +681,7 @@ async def rpc(self, msg: dict[str, _RichJson]) -> _Json:
return result

@classmethod
def from_json(cls, data: Type | str | dict[str, Any] | list[Any]) -> Type | None:
def from_json(cls, data: Type | str | dict[str, Any] | list[Any]) -> Self | None:
def _parse_nested_list_entry(expr, result_dict):
if isinstance(expr, str):
if ">" in expr or ">=" in expr:
Expand Down
9 changes: 5 additions & 4 deletions juju/client/gocookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import time

import pyrfc3339
from backports.datetime_fromisoformat import datetime_fromisoformat


class GoCookieJar(cookiejar.FileCookieJar):
Expand Down Expand Up @@ -52,7 +52,7 @@ def go_to_py_cookie(go_cookie):
"""Convert a Go-style JSON-unmarshaled cookie into a Python cookie"""
expires = None
if go_cookie.get("Expires") is not None:
t = pyrfc3339.parse(go_cookie["Expires"])
t = datetime_fromisoformat(go_cookie["Expires"])
expires = t.timestamp()
return cookiejar.Cookie(
version=0,
Expand Down Expand Up @@ -101,8 +101,9 @@ def py_to_go_cookie(py_cookie):
if py_cookie.path_specified:
go_cookie["Path"] = py_cookie.path
if py_cookie.expires is not None:
unix_time = datetime.datetime.fromtimestamp(py_cookie.expires)
# Note: fromtimestamp bizarrely produces a time without
# a time zone, so we need to use accept_naive.
go_cookie["Expires"] = pyrfc3339.generate(unix_time, accept_naive=True)
go_cookie["Expires"] = datetime.datetime.fromtimestamp(
py_cookie.expires
).isoformat()
return go_cookie
6 changes: 3 additions & 3 deletions juju/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
import typing

import pyrfc3339
from backports.datetime_fromisoformat import datetime_fromisoformat

from juju.utils import block_until, juju_ssh_key_paths

Expand Down Expand Up @@ -239,7 +239,7 @@ def agent_status(self):
@property
def agent_status_since(self):
"""Get the time when the `agent_status` was last updated."""
return pyrfc3339.parse(self.safe_data["agent-status"]["since"])
return datetime_fromisoformat(self.safe_data["agent-status"]["since"])

@property
def agent_version(self):
Expand All @@ -266,7 +266,7 @@ def status_message(self):
@property
def status_since(self):
"""Get the time when the `status` was last updated."""
return pyrfc3339.parse(self.safe_data["instance-status"]["since"])
return datetime_fromisoformat(self.safe_data["instance-status"]["since"])

@property
def dns_name(self):
Expand Down
6 changes: 3 additions & 3 deletions juju/unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import logging

import pyrfc3339
from backports.datetime_fromisoformat import datetime_fromisoformat

from juju.errors import JujuAPIError, JujuError

Expand All @@ -27,7 +27,7 @@ def agent_status(self):
@property
def agent_status_since(self):
"""Get the time when the `agent_status` was last updated."""
return pyrfc3339.parse(self.safe_data["agent-status"]["since"])
return datetime_fromisoformat(self.safe_data["agent-status"]["since"])

@property
def is_subordinate(self):
Expand All @@ -54,7 +54,7 @@ def workload_status(self):
@property
def workload_status_since(self):
"""Get the time when the `workload_status` was last updated."""
return pyrfc3339.parse(self.safe_data["workload-status"]["since"])
return datetime_fromisoformat(self.safe_data["workload-status"]["since"])

@property
def workload_status_message(self):
Expand Down
4 changes: 2 additions & 2 deletions juju/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import logging

import pyrfc3339
from backports.datetime_fromisoformat import datetime_fromisoformat

from . import errors, tag
from .client import client
Expand Down Expand Up @@ -31,7 +31,7 @@ def display_name(self):

@property
def last_connection(self):
return pyrfc3339.parse(self._user_info.last_connection)
return datetime_fromisoformat(self._user_info.last_connection)

@property
def access(self):
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ classifiers = [
]
dependencies = [
"macaroonbakery>=1.1,<2.0",
"pyRFC3339>=1.0,<2.0",
"pyyaml>=5.1.2",
"websockets>=13.0.1",
"paramiko>=2.4.0",
Expand All @@ -35,12 +34,13 @@ dependencies = [
"packaging",
"typing-extensions>=4.5.0",
'backports.strenum>=1.3.1; python_version < "3.11"',
"backports-datetime-fromisoformat>=2.0.2",
]
[project.optional-dependencies]
dev = [
"typing-inspect",
"pytest",
"pytest-asyncio",
"pytest-asyncio <= 0.25.0", # https://github.com/pytest-dev/pytest-asyncio/issues/1039
"Twine",
"freezegun",
]
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
package_data={"juju": ["py.typed"]},
install_requires=[
"macaroonbakery>=1.1,<2.0",
"pyRFC3339>=1.0,<2.0",
"pyyaml>=5.1.2",
"websockets>=13.0.1",
"paramiko>=2.4.0",
Expand All @@ -33,12 +32,13 @@
"packaging",
"typing-extensions>=4.5.0",
'backports.strenum>=1.3.1; python_version < "3.11"',
"backports-datetime-fromisoformat>=2.0.2",
],
extras_require={
"dev": [
"typing-inspect",
"pytest",
"pytest-asyncio",
"pytest-asyncio <= 0.25.0", # https://github.com/pytest-dev/pytest-asyncio/issues/1039
"Twine",
"freezegun",
]
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/test_gocookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
import unittest
import urllib.request

import pyrfc3339
from backports.datetime_fromisoformat import datetime_fromisoformat

from juju.client.gocookies import GoCookieJar

# cookie_content holds the JSON contents of a Go-produced
# cookie file (reformatted so it's not all on one line but
# otherwise unchanged).

cookie_content = """
[
{
Expand Down Expand Up @@ -223,7 +224,7 @@ def test_expiry_time(self):
]"""
jar = self.load_jar(content)
got_expires = tuple(jar)[0].expires
want_expires = int(pyrfc3339.parse("2345-11-15T18:16:08Z").timestamp())
want_expires = int(datetime_fromisoformat("2345-11-15T18:16:08Z").timestamp())
self.assertEqual(got_expires, want_expires)

def load_jar(self, content):
Expand Down
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ commands =

[testenv:unit]
envdir = {toxworkdir}/py3
deps =
backports-datetime-fromisoformat
commands =
pytest {toxinidir}/tests/unit {posargs}

Expand Down