diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dba19a8..b5d9989 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,6 +19,10 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: inclusive-naming-check: name: Inclusive naming check diff --git a/charmcraft.yaml b/charmcraft.yaml index eb3df58..b628329 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -25,8 +25,6 @@ parts: build-snaps: - astral-uv charm-requirements: ["requirements.txt"] - charm-binary-python-packages: - - rpds_py ~= 0.22.3 override-build: | just requirements craftctl default @@ -42,32 +40,33 @@ subordinate: true requires: filesystem: interface: filesystem_info + limit: 1 juju-info: interface: juju-info scope: container config: options: - mounts: - default: "{}" + mountpoint: + description: Location to mount the filesystem on the machine. type: string + noexec: + default: false description: | - Information to mount filesystems on the machine. This is specified as a JSON object string. - Example usage: - ```bash - $ juju config filesystem-client \ - mounts=< None: from abc import ABC, abstractmethod from dataclasses import dataclass from ipaddress import AddressValueError, IPv6Address -from typing import List, Optional, TypeVar, Self +from typing import List, Optional, TypeVar from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse, urlunsplit import ops @@ -354,9 +354,7 @@ def from_uri(cls, uri: str, _model: Model) -> "NfsInfo": info = _UriData.from_uri(uri) if info.scheme != cls.filesystem_type(): - raise ParseUriError( - "could not parse uri with incompatible scheme into `NfsInfo`" - ) + raise ParseUriError("could not parse uri with incompatible scheme into `NfsInfo`") path = info.path @@ -582,16 +580,14 @@ class FilesystemRequires(_BaseInterface): def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) self.framework.observe(charm.on[relation_name].relation_changed, self._on_relation_changed) - self.framework.observe( - charm.on[relation_name].relation_departed, self._on_relation_departed - ) + self.framework.observe(charm.on[relation_name].relation_broken, self._on_relation_broken) def _on_relation_changed(self, event: RelationChangedEvent) -> None: """Handle when the databag between client and server has been updated.""" _logger.debug("emitting `MountFilesystem` event from `RelationChanged` hook") self.on.mount_filesystem.emit(event.relation, app=event.app, unit=event.unit) - def _on_relation_departed(self, event: RelationDepartedEvent) -> None: + def _on_relation_broken(self, event: RelationDepartedEvent) -> None: """Handle when server departs integration.""" _logger.debug("emitting `UmountFilesystem` event from `RelationDeparted` hook") self.on.umount_filesystem.emit(event.relation, app=event.app, unit=event.unit) diff --git a/pyproject.toml b/pyproject.toml index 19b0bed..71008d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,7 @@ name = "filesystem-client" version = "0.0" requires-python = "==3.12.*" dependencies = [ - "ops ~= 2.8", - "jsonschema ~= 4.23.0", - "rpds_py ~= 0.22.3" + "ops ~= 2.8" ] [project.optional-dependencies] diff --git a/src/charm.py b/src/charm.py index 93c0e5e..648ac34 100755 --- a/src/charm.py +++ b/src/charm.py @@ -4,42 +4,40 @@ """Charm for the filesystem client.""" -import json import logging -from collections import Counter +from dataclasses import dataclass import ops from charms.filesystem_client.v0.filesystem_info import FilesystemRequires -from jsonschema import ValidationError, validate from utils.manager import MountsManager -logger = logging.getLogger(__name__) - -CONFIG_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "additionalProperties": { - "type": "object", - "required": ["mountpoint"], - "properties": { - "mountpoint": {"type": "string"}, - "noexec": {"type": "boolean"}, - "nosuid": {"type": "boolean"}, - "nodev": {"type": "boolean"}, - "read-only": {"type": "boolean"}, - }, - }, -} +_logger = logging.getLogger(__name__) class StopCharmError(Exception): """Exception raised when a method needs to finish the execution of the charm code.""" - def __init__(self, status: ops.StatusBase): + def __init__(self, status: ops.StatusBase) -> None: self.status = status +@dataclass(frozen=True) +class CharmConfig: + """Configuration for the charm.""" + + mountpoint: str + """Location to mount the filesystem on the machine.""" + noexec: bool + """Block execution of binaries on the filesystem.""" + nosuid: bool + """Do not honor suid and sgid bits on the filesystem.""" + nodev: bool + """Blocking interpretation of character and/or block devices on the filesystem.""" + read_only: bool + """Mount filesystem as read-only.""" + + # Trying to use a delta charm (one method per event) proved to be a bit unwieldy, since # we would have to handle multiple updates at once: # - mount requests @@ -51,11 +49,11 @@ def __init__(self, status: ops.StatusBase): # mount requests. # # A holistic charm (one method for all events) was a lot easier to deal with, -# simplifying the code to handle all the multiple relations. +# simplifying the code to handle all the events. class FilesystemClientCharm(ops.CharmBase): """Charm the application.""" - def __init__(self, framework: ops.Framework): + def __init__(self, framework: ops.Framework) -> None: super().__init__(framework) self._filesystems = FilesystemRequires(self, "filesystem") @@ -66,71 +64,66 @@ def __init__(self, framework: ops.Framework): framework.observe(self._filesystems.on.mount_filesystem, self._handle_event) framework.observe(self._filesystems.on.umount_filesystem, self._handle_event) - def _handle_event(self, event: ops.EventBase) -> None: # noqa: C901 + def _handle_event(self, event: ops.EventBase) -> None: + """Handle a Juju event.""" try: self.unit.status = ops.MaintenanceStatus("Updating status.") + # CephFS is not supported on LXD containers. + if not self._mounts_manager.supported(): + self.unit.status = ops.BlockedStatus("Cannot mount filesystems on LXD containers.") + return + self._ensure_installed() config = self._get_config() self._mount_filesystems(config) except StopCharmError as e: # This was the cleanest way to ensure the inner methods can still return prematurely # when an error occurs. - self.app.status = e.status + self.unit.status = e.status return - self.unit.status = ops.ActiveStatus("Mounted filesystems.") + self.unit.status = ops.ActiveStatus(f"Mounted filesystem at `{config.mountpoint}`.") - def _ensure_installed(self): + def _ensure_installed(self) -> None: """Ensure the required packages are installed into the unit.""" if not self._mounts_manager.installed: self.unit.status = ops.MaintenanceStatus("Installing required packages.") self._mounts_manager.install() - def _get_config(self) -> dict[str, dict[str, str | bool]]: + def _get_config(self) -> CharmConfig: """Get and validate the configuration of the charm.""" - try: - config = json.loads(str(self.config.get("mounts", ""))) - validate(config, CONFIG_SCHEMA) - config: dict[str, dict[str, str | bool]] = config - for fs, opts in config.items(): - for opt in ["noexec", "nosuid", "nodev", "read-only"]: - opts[opt] = opts.get(opt, False) - return config - except (json.JSONDecodeError, ValidationError) as e: + if not (mountpoint := self.config.get("mountpoint")): + raise StopCharmError(ops.BlockedStatus("Missing `mountpoint` in config.")) + + return CharmConfig( + mountpoint=str(mountpoint), + noexec=bool(self.config.get("noexec")), + nosuid=bool(self.config.get("nosuid")), + nodev=bool(self.config.get("nodev")), + read_only=bool(self.config.get("read-only")), + ) + + def _mount_filesystems(self, config: CharmConfig) -> None: + """Mount the filesystem for the charm.""" + endpoints = self._filesystems.endpoints + if not endpoints: raise StopCharmError( - ops.BlockedStatus(f"invalid configuration for option `mounts`. reason:\n{e}") + ops.BlockedStatus("Waiting for an integration with a filesystem provider.") ) - def _mount_filesystems(self, config: dict[str, dict[str, str | bool]]): - """Mount all available filesystems for the charm.""" - endpoints = self._filesystems.endpoints - for fs_type, count in Counter( - [endpoint.info.filesystem_type() for endpoint in endpoints] - ).items(): - if count > 1: - raise StopCharmError( - ops.BlockedStatus(f"Too many relations for mount type `{fs_type}`.") - ) + # This is limited to 1 relation. + endpoint = endpoints[0] - self.unit.status = ops.MaintenanceStatus("Ensuring filesystems are mounted.") + self.unit.status = ops.MaintenanceStatus("Mounting filesystem.") with self._mounts_manager.mounts() as mounts: - for endpoint in endpoints: - fs_type = endpoint.info.filesystem_type() - if not (options := config.get(fs_type)): - raise StopCharmError( - ops.BlockedStatus(f"Missing configuration for mount type `{fs_type}`.") - ) - - mountpoint = str(options["mountpoint"]) - - opts = [] - opts.append("noexec" if options.get("noexec") else "exec") - opts.append("nosuid" if options.get("nosuid") else "suid") - opts.append("nodev" if options.get("nodev") else "dev") - opts.append("ro" if options.get("read-only") else "rw") - mounts.add(info=endpoint.info, mountpoint=mountpoint, options=opts) + opts = [] + opts.append("noexec" if config.noexec else "exec") + opts.append("nosuid" if config.nosuid else "suid") + opts.append("nodev" if config.nodev else "dev") + opts.append("ro" if config.read_only else "rw") + mounts.add(info=endpoint.info, mountpoint=config.mountpoint, options=opts) if __name__ == "__main__": # pragma: nocover diff --git a/src/utils/manager.py b/src/utils/manager.py index 3a9fe7a..a542b2d 100644 --- a/src/utils/manager.py +++ b/src/utils/manager.py @@ -8,9 +8,9 @@ import os import pathlib import subprocess +from collections.abc import Iterable from dataclasses import dataclass from ipaddress import AddressValueError, IPv6Address -from typing import Generator, List, Optional, Union import charms.operator_libs_linux.v0.apt as apt import charms.operator_libs_linux.v1.systemd as systemd @@ -24,16 +24,16 @@ class Error(Exception): """Raise if Storage client manager encounters an error.""" @property - def name(self): + def name(self) -> str: """Get a string representation of the error plus class name.""" return f"<{type(self).__module__}.{type(self).__name__}>" @property - def message(self): + def message(self) -> str: """Return the message passed as an argument.""" return self.args[0] - def __repr__(self): + def __repr__(self) -> str: """Return the string representation of the error.""" return f"<{type(self).__module__}.{type(self).__name__} {self.args}>" @@ -63,16 +63,14 @@ class _MountInfo: class Mounts: """Collection of mounts that need to be managed by the `MountsManager`.""" - _mounts: dict[str, _MountInfo] - - def __init__(self): - self._mounts = {} + def __init__(self) -> None: + self._mounts: dict[str, _MountInfo] = {} def add( self, info: FilesystemInfo, - mountpoint: Union[str, os.PathLike], - options: Optional[List[str]] = None, + mountpoint: str | os.PathLike, + options: list[str] | None = None, ) -> None: """Add a mount to the list of managed mounts. @@ -96,14 +94,14 @@ def add( class MountsManager: """Manager for mounted filesystems in the current system.""" - def __init__(self, charm: ops.CharmBase): + def __init__(self, charm: ops.CharmBase) -> None: # Lazily initialized self._pkgs = None self._master_file = pathlib.Path(f"/etc/auto.master.d/{charm.app.name}.autofs") self._autofs_file = pathlib.Path(f"/etc/auto.{charm.app.name}") @property - def _packages(self) -> List[apt.DebianPackage]: + def _packages(self) -> list[apt.DebianPackage]: """List of packages required by the client.""" if not self._pkgs: self._pkgs = [ @@ -119,12 +117,12 @@ def installed(self) -> bool: if not pkg.present: return False - if not self._master_file.exists or not self._autofs_file.exists: + if not self._master_file.exists() or not self._autofs_file.exists(): return False return True - def install(self): + def install(self) -> None: """Install the required mount packages. Raises: @@ -134,9 +132,7 @@ def install(self): for pkg in self._packages: pkg.ensure(apt.PackageState.Present) except (apt.PackageError, apt.PackageNotFoundError) as e: - _logger.error( - f"failed to change the state of the required packages. reason:\n{e.message}" - ) + _logger.error("failed to change the state of the required packages", exc_info=e) raise Error(e.message) try: @@ -144,7 +140,7 @@ def install(self): self._autofs_file.touch(mode=0o600) self._master_file.write_text(f"/- {self._autofs_file}") except IOError as e: - _logger.error(f"failed to create the required autofs files. reason:\n{e}") + _logger.error("failed to create the required autofs files", exc_info=e) raise Error("failed to create the required autofs files") def supported(self) -> bool: @@ -159,11 +155,11 @@ def supported(self) -> bool: else: return True except subprocess.CalledProcessError: - _logger.warning("Could not detect execution in virtualized environment") + _logger.warning("could not detect execution in virtualized environment") return True @contextlib.contextmanager - def mounts(self, force_mount=False) -> Generator[Mounts, None, None]: + def mounts(self, force_mount=False) -> Iterable[Mounts]: """Get the list of `Mounts` that need to be managed by the `MountsManager`. It will initially contain no mounts, and any mount that is added to @@ -195,9 +191,7 @@ def mounts(self, force_mount=False) -> Generator[Mounts, None, None]: self._autofs_file.write_text(new_autofs) systemd.service_reload("autofs", restart_on_failure=True) except systemd.SystemdError as e: - _logger.error(f"failed to mount filesystems. reason:\n{e}") - if "Operation not permitted" in str(e) and not self.supported(): - raise Error("mounting shares not supported on LXD containers") + _logger.error("failed to mount filesystems", exc_info=e) raise Error("failed to mount filesystems") diff --git a/tests/integration/server/lib/charms/filesystem_client/v0/filesystem_info.py b/tests/integration/server/lib/charms/filesystem_client/v0/filesystem_info.py index d0e1ea7..dbce045 100644 --- a/tests/integration/server/lib/charms/filesystem_client/v0/filesystem_info.py +++ b/tests/integration/server/lib/charms/filesystem_client/v0/filesystem_info.py @@ -100,7 +100,7 @@ def _on_start(self, event: ops.StartEvent) -> None: from abc import ABC, abstractmethod from dataclasses import dataclass from ipaddress import AddressValueError, IPv6Address -from typing import List, Optional, TypeVar, Self +from typing import List, Optional, TypeVar from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse, urlunsplit import ops @@ -354,9 +354,7 @@ def from_uri(cls, uri: str, _model: Model) -> "NfsInfo": info = _UriData.from_uri(uri) if info.scheme != cls.filesystem_type(): - raise ParseUriError( - "could not parse uri with incompatible scheme into `NfsInfo`" - ) + raise ParseUriError("could not parse uri with incompatible scheme into `NfsInfo`") path = info.path @@ -582,16 +580,14 @@ class FilesystemRequires(_BaseInterface): def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) self.framework.observe(charm.on[relation_name].relation_changed, self._on_relation_changed) - self.framework.observe( - charm.on[relation_name].relation_departed, self._on_relation_departed - ) + self.framework.observe(charm.on[relation_name].relation_broken, self._on_relation_broken) def _on_relation_changed(self, event: RelationChangedEvent) -> None: """Handle when the databag between client and server has been updated.""" _logger.debug("emitting `MountFilesystem` event from `RelationChanged` hook") self.on.mount_filesystem.emit(event.relation, app=event.app, unit=event.unit) - def _on_relation_departed(self, event: RelationDepartedEvent) -> None: + def _on_relation_broken(self, event: RelationDepartedEvent) -> None: """Handle when server departs integration.""" _logger.debug("emitting `UmountFilesystem` event from `RelationDeparted` hook") self.on.umount_filesystem.emit(event.relation, app=event.app, unit=event.unit) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 555031d..7ccdfb8 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -34,7 +34,7 @@ @pytest.mark.abort_on_fail @pytest.mark.order(1) -async def test_build_and_deploy(ops_test: OpsTest): +async def test_build_and_deploy(ops_test: OpsTest) -> None: """Build the charm-under-test and deploy it together with related charms. Assert on the unit status before any relations/configurations take place. @@ -44,74 +44,87 @@ async def test_build_and_deploy(ops_test: OpsTest): server = await ops_test.build_charm("./tests/integration/server") # Deploy the charm and wait for active/idle status - await asyncio.gather( - ops_test.model.deploy( - "ubuntu", - application_name="ubuntu", - base="ubuntu@24.04", - constraints=juju.constraints.parse("virt-type=virtual-machine"), - ), - ops_test.model.deploy( - charm, - application_name=APP_NAME, - num_units=0, - config={ - "mounts": """ - { - "nfs": { - "mountpoint": "/nfs", - "nodev": true, - "read-only": true + async with asyncio.TaskGroup() as tg: + tg.create_task( + ops_test.model.deploy( + "ubuntu", + application_name="ubuntu", + base="ubuntu@24.04", + constraints=juju.constraints.parse("virt-type=virtual-machine"), + ) + ) + tg.create_task( + ops_test.model.deploy( + charm, + application_name=APP_NAME, + num_units=0, + ) + ) + tg.create_task( + ops_test.model.deploy( + server, + application_name="nfs-server", + config={ + "type": "nfs", }, - "cephfs": { - "mountpoint": "/cephfs", - "noexec": true, - "nosuid": true, - "nodev": false - } - } - """ - }, - ), - ops_test.model.deploy( - server, - application_name="nfs-server", - config={ - "type": "nfs", - }, - constraints=juju.constraints.parse("virt-type=virtual-machine"), - ), - ops_test.model.deploy( - server, - application_name="cephfs-server", - config={ - "type": "cephfs", - }, - constraints=juju.constraints.parse("virt-type=virtual-machine root-disk=50G mem=8G"), - ), - ops_test.model.wait_for_idle( - apps=["nfs-server", "cephfs-server", "ubuntu"], - status="active", - raise_on_blocked=True, - timeout=1000, - ), - ) + constraints=juju.constraints.parse("virt-type=virtual-machine"), + ) + ) + tg.create_task( + ops_test.model.deploy( + server, + application_name="cephfs-server", + config={ + "type": "cephfs", + }, + constraints=juju.constraints.parse( + "virt-type=virtual-machine root-disk=50G mem=8G" + ), + ) + ) + tg.create_task( + ops_test.model.wait_for_idle( + apps=["nfs-server", "cephfs-server", "ubuntu"], + status="active", + raise_on_blocked=True, + raise_on_error=True, + timeout=1000, + ) + ) @pytest.mark.abort_on_fail @pytest.mark.order(2) -async def test_integrate(ops_test: OpsTest): - await ops_test.model.integrate(f"{APP_NAME}:juju-info", "ubuntu:juju-info") - await ops_test.model.integrate(f"{APP_NAME}:filesystem", "nfs-server:filesystem") - await ops_test.model.integrate(f"{APP_NAME}:filesystem", "cephfs-server:filesystem") - - await ops_test.model.wait_for_idle( - apps=[APP_NAME, "ubuntu", "nfs-server", "cephfs-server"], status="active", timeout=1000 +async def test_integrate(ops_test: OpsTest) -> None: + async with asyncio.TaskGroup() as tg: + tg.create_task(ops_test.model.integrate(f"{APP_NAME}:juju-info", "ubuntu:juju-info")) + tg.create_task( + ops_test.model.wait_for_idle(apps=["ubuntu"], status="active", raise_on_error=True) + ) + tg.create_task( + ops_test.model.wait_for_idle(apps=[APP_NAME], status="blocked", raise_on_error=True) + ) + + assert ( + ops_test.model.applications[APP_NAME].units[0].workload_status_message + == "Missing `mountpoint` in config." ) +@pytest.mark.abort_on_fail @pytest.mark.order(3) -async def test_nfs_files(ops_test: OpsTest): +async def test_nfs(ops_test: OpsTest) -> None: + async with asyncio.TaskGroup() as tg: + tg.create_task(ops_test.model.integrate(f"{APP_NAME}:filesystem", "nfs-server:filesystem")) + tg.create_task( + ops_test.model.applications[APP_NAME].set_config( + {"mountpoint": "/nfs", "nodev": "true", "read-only": "true"} + ) + ) + tg.create_task( + ops_test.model.wait_for_idle(apps=[APP_NAME], status="active", raise_on_error=True) + ) + unit = ops_test.model.applications["ubuntu"].units[0] result = (await unit.ssh("ls /nfs")).strip("\n") assert "test-0" in result @@ -119,8 +132,34 @@ async def test_nfs_files(ops_test: OpsTest): assert "test-2" in result +@pytest.mark.abort_on_fail @pytest.mark.order(4) -async def test_cephfs_files(ops_test: OpsTest): +async def test_cephfs(ops_test: OpsTest) -> None: + # Ensure the relation is removed before the config changes. + # This guarantees that the new mountpoint is fresh. + async with asyncio.TaskGroup() as tg: + tg.create_task( + ops_test.model.applications[APP_NAME].remove_relation( + "filesystem", "nfs-server:filesystem" + ) + ) + tg.create_task( + ops_test.model.wait_for_idle(apps=[APP_NAME], status="blocked", raise_on_error=True) + ) + + async with asyncio.TaskGroup() as tg: + tg.create_task( + ops_test.model.applications[APP_NAME].set_config( + {"mountpoint": "/cephfs", "noexec": "true", "nosuid": "true", "nodev": "false"} + ) + ) + tg.create_task( + ops_test.model.integrate(f"{APP_NAME}:filesystem", "cephfs-server:filesystem") + ) + tg.create_task( + ops_test.model.wait_for_idle(apps=[APP_NAME], status="active", raise_on_error=True) + ) + unit = ops_test.model.applications["ubuntu"].units[0] result = (await unit.ssh("ls /cephfs")).strip("\n") assert "test-0" in result diff --git a/uv.lock b/uv.lock index d065cad..7e10692 100644 --- a/uv.lock +++ b/uv.lock @@ -10,15 +10,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] -[[package]] -name = "attrs" -version = "24.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, -] - [[package]] name = "bcrypt" version = "4.2.1" @@ -203,9 +194,7 @@ name = "filesystem-client" version = "0.0" source = { virtual = "." } dependencies = [ - { name = "jsonschema" }, { name = "ops" }, - { name = "rpds-py" }, ] [package.optional-dependencies] @@ -225,7 +214,6 @@ dev = [ requires-dist = [ { name = "codespell", marker = "extra == 'dev'" }, { name = "coverage", extras = ["toml"], marker = "extra == 'dev'" }, - { name = "jsonschema", specifier = "~=4.23.0" }, { name = "juju", marker = "extra == 'dev'" }, { name = "ops", specifier = "~=2.8" }, { name = "ops", extras = ["testing"], marker = "extra == 'dev'" }, @@ -233,7 +221,6 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-operator", marker = "extra == 'dev'" }, { name = "pytest-order", marker = "extra == 'dev'" }, - { name = "rpds-py", specifier = "~=0.22.3" }, { name = "ruff", marker = "extra == 'dev'" }, ] @@ -338,33 +325,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] -[[package]] -name = "jsonschema" -version = "4.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2024.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, -] - [[package]] name = "juju" version = "3.6.0.0" @@ -581,16 +541,16 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.1" +version = "5.29.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d2/4f/1639b7b1633d8fd55f216ba01e21bf2c43384ab25ef3ddb35d85a52033e8/protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", size = 424965 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/73/4e6295c1420a9d20c9c351db3a36109b4c9aa601916cb7c6871e3196a1ca/protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e", size = 424901 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/c7/28669b04691a376cf7d0617d612f126aa0fff763d57df0142f9bf474c5b8/protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", size = 422706 }, - { url = "https://files.pythonhosted.org/packages/e3/33/dc7a7712f457456b7e0b16420ab8ba1cc8686751d3f28392eb43d0029ab9/protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", size = 434505 }, - { url = "https://files.pythonhosted.org/packages/e5/39/44239fb1c6ec557e1731d996a5de89a9eb1ada7a92491fcf9c5d714052ed/protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18", size = 417822 }, - { url = "https://files.pythonhosted.org/packages/fb/4a/ec56f101d38d4bef2959a9750209809242d86cf8b897db00f2f98bfa360e/protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", size = 319572 }, - { url = "https://files.pythonhosted.org/packages/04/52/c97c58a33b3d6c89a8138788576d372a90a6556f354799971c6b4d16d871/protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", size = 319671 }, - { url = "https://files.pythonhosted.org/packages/3b/24/c8c49df8f6587719e1d400109b16c10c6902d0c9adddc8fff82840146f99/protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", size = 172547 }, + { url = "https://files.pythonhosted.org/packages/f3/42/6db5387124708d619ffb990a846fb123bee546f52868039f8fa964c5bc54/protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851", size = 422697 }, + { url = "https://files.pythonhosted.org/packages/6c/38/2fcc968b377b531882d6ab2ac99b10ca6d00108394f6ff57c2395fb7baff/protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9", size = 434495 }, + { url = "https://files.pythonhosted.org/packages/cb/26/41debe0f6615fcb7e97672057524687ed86fcd85e3da3f031c30af8f0c51/protobuf-5.29.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb", size = 417812 }, + { url = "https://files.pythonhosted.org/packages/e4/20/38fc33b60dcfb380507b99494aebe8c34b68b8ac7d32808c4cebda3f6f6b/protobuf-5.29.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e", size = 319562 }, + { url = "https://files.pythonhosted.org/packages/90/4d/c3d61e698e0e41d926dbff6aa4e57428ab1a6fc3b5e1deaa6c9ec0fd45cf/protobuf-5.29.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e", size = 319662 }, + { url = "https://files.pythonhosted.org/packages/f3/fd/c7924b4c2a1c61b8f4b64edd7a31ffacf63432135a2606f03a2f0d75a750/protobuf-5.29.2-py3-none-any.whl", hash = "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181", size = 172539 }, ] [[package]] @@ -697,15 +657,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.390" +version = "1.1.391" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/42/1e0392f35dd275f9f775baf7c86407cef7f6a0d9b8e099a93e5422a7e571/pyright-1.1.390.tar.gz", hash = "sha256:aad7f160c49e0fbf8209507a15e17b781f63a86a1facb69ca877c71ef2e9538d", size = 21950 } +sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/20/3f492ca789fb17962ad23619959c7fa642082621751514296c58de3bb801/pyright-1.1.390-py3-none-any.whl", hash = "sha256:ecebfba5b6b50af7c1a44c2ba144ba2ab542c227eb49bc1f16984ff714e0e110", size = 18579 }, + { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, ] [[package]] @@ -802,19 +762,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, ] -[[package]] -name = "referencing" -version = "0.35.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/73ca1f8e72fff6fa52119dbd185f73a907b1989428917b24cff660129b6d/referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", size = 62991 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de", size = 26684 }, -] - [[package]] name = "requests" version = "2.32.3" @@ -843,27 +790,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, ] -[[package]] -name = "rpds-py" -version = "0.22.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/80/cce854d0921ff2f0a9fa831ba3ad3c65cee3a46711addf39a2af52df2cfd/rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", size = 26771 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/47/3383ee3bd787a2a5e65a9b9edc37ccf8505c0a00170e3a5e6ea5fbcd97f7/rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", size = 352334 }, - { url = "https://files.pythonhosted.org/packages/40/14/aa6400fa8158b90a5a250a77f2077c0d0cd8a76fce31d9f2b289f04c6dec/rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", size = 342111 }, - { url = "https://files.pythonhosted.org/packages/7d/06/395a13bfaa8a28b302fb433fb285a67ce0ea2004959a027aea8f9c52bad4/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", size = 384286 }, - { url = "https://files.pythonhosted.org/packages/43/52/d8eeaffab047e6b7b7ef7f00d5ead074a07973968ffa2d5820fa131d7852/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e", size = 391739 }, - { url = "https://files.pythonhosted.org/packages/83/31/52dc4bde85c60b63719610ed6f6d61877effdb5113a72007679b786377b8/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", size = 427306 }, - { url = "https://files.pythonhosted.org/packages/70/d5/1bab8e389c2261dba1764e9e793ed6830a63f830fdbec581a242c7c46bda/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", size = 442717 }, - { url = "https://files.pythonhosted.org/packages/82/a1/a45f3e30835b553379b3a56ea6c4eb622cf11e72008229af840e4596a8ea/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", size = 385721 }, - { url = "https://files.pythonhosted.org/packages/a6/27/780c942de3120bdd4d0e69583f9c96e179dfff082f6ecbb46b8d6488841f/rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", size = 415824 }, - { url = "https://files.pythonhosted.org/packages/94/0b/aa0542ca88ad20ea719b06520f925bae348ea5c1fdf201b7e7202d20871d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", size = 561227 }, - { url = "https://files.pythonhosted.org/packages/0d/92/3ed77d215f82c8f844d7f98929d56cc321bb0bcfaf8f166559b8ec56e5f1/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", size = 587424 }, - { url = "https://files.pythonhosted.org/packages/09/42/cacaeb047a22cab6241f107644f230e2935d4efecf6488859a7dd82fc47d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", size = 555953 }, - { url = "https://files.pythonhosted.org/packages/e6/52/c921dc6d5f5d45b212a456c1f5b17df1a471127e8037eb0972379e39dff4/rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", size = 221339 }, - { url = "https://files.pythonhosted.org/packages/f2/c7/f82b5be1e8456600395366f86104d1bd8d0faed3802ad511ef6d60c30d98/rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", size = 235786 }, -] - [[package]] name = "rsa" version = "4.9"