diff --git a/interfaces/temporal-host-info/CHANGELOG.md b/interfaces/temporal-host-info/CHANGELOG.md new file mode 100644 index 000000000..ba9d0ef08 --- /dev/null +++ b/interfaces/temporal-host-info/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 - 25 November 2025 + +Initial release. diff --git a/interfaces/temporal-host-info/README.md b/interfaces/temporal-host-info/README.md new file mode 100644 index 000000000..2bcf32050 --- /dev/null +++ b/interfaces/temporal-host-info/README.md @@ -0,0 +1,11 @@ +# charmlibs.interfaces.temporal_host_info + +The `temporal-host-info` interface library. + +To install, add `charmlibs-interfaces-temporal-host-info` to your Python dependencies. Then in your Python code, import as: + +```py +from charmlibs.interfaces import temporal_host_info +``` + +See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/temporal_host_info) for more. diff --git a/interfaces/temporal-host-info/pyproject.toml b/interfaces/temporal-host-info/pyproject.toml new file mode 100644 index 000000000..9deea987e --- /dev/null +++ b/interfaces/temporal-host-info/pyproject.toml @@ -0,0 +1,73 @@ +[project] +name = "charmlibs-interfaces-temporal-host-info" +description = "The charmlibs.interfaces.temporal_host_info package." +readme = "README.md" +requires-python = ">=3.10" +authors = [ + {name="The Charm Engineering team at Canonical"}, +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Development Status :: 5 - Production/Stable", +] +dynamic = ["version"] +dependencies = [ + "jubilant>=1.5.0", + # "ops", + "ops==2.21.1", +] + +[dependency-groups] +lint = [ # installed for `just lint interfaces/temporal-host-info` (unit, functional, and integration are also installed) + # "typing_extensions", +] +unit = [ # installed for `just unit interfaces/temporal-host-info` + "ops[testing]", +] +functional = [ # installed for `just functional interfaces/temporal-host-info` +] +integration = [ # installed for `just integration interfaces/temporal-host-info` + "jubilant", +] + +[project.urls] +"Repository" = "https://github.com/canonical/charmlibs" +"Issues" = "https://github.com/canonical/charmlibs/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/charmlibs"] + +[tool.hatch.version] +path = "src/charmlibs/interfaces/temporal_host_info/_version.py" + +[tool.ruff] +extend = "../../pyproject.toml" +src = ["src", "tests/unit", "tests/functional", "tests/integration"] # correctly sort local imports in tests + +[tool.ruff.lint.extend-per-file-ignores] +# add additional per-file-ignores here to avoid overriding repo-level config +"tests/**/*" = [ + # "E501", # line too long +] + +[tool.pyright] +extends = "../../pyproject.toml" +include = ["src", "tests"] +pythonVersion = "3.10" # check no python > 3.10 features are used + +[tool.charmlibs.functional] +ubuntu = [] # ubuntu versions to run functional tests with, e.g. "24.04" (defaults to just "latest") +pebble = [] # pebble versions to run functional tests with, e.g. "v1.0.0", "master" (defaults to no pebble versions) +sudo = false # whether to run functional tests with sudo (defaults to false) + +[tool.charmlibs.integration] +# tags to run integration tests with (defaults to running once with no tag, i.e. tags = ['']) +# Available in CI in tests/integration/pack.sh and integration tests as CHARMLIBS_TAG +tags = [] # Not used by the pack.sh and integration tests generated by the template diff --git a/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/__init__.py b/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/__init__.py new file mode 100644 index 000000000..8e98e19b2 --- /dev/null +++ b/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Relation management for temporal-host-info interface.""" + +from ._temporal_host_info import ( + TemporalHostInfoProvider, + TemporalHostInfoRequirer, +) +from ._version import __version__ as __version__ + +__all__ = [ + 'TemporalHostInfoProvider', + 'TemporalHostInfoRequirer', +] diff --git a/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/_temporal_host_info.py b/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/_temporal_host_info.py new file mode 100644 index 000000000..f2cf89335 --- /dev/null +++ b/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/_temporal_host_info.py @@ -0,0 +1,188 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Relation management for temporal-host-info interface.""" + +import logging + +from ops import ( + ConfigChangedEvent, + Handle, + LeaderElectedEvent, + RelationChangedEvent, + RelationJoinedEvent, + framework, +) +from ops.charm import CharmBase +from ops.framework import EventBase, EventSource, ObjectEvents +from ops.model import ActiveStatus, Relation, WaitingStatus + +logger = logging.getLogger(__name__) + +RELATION_NAME = 'temporal-host-info' + + +class TemporalHostInfoProvider(framework.Object): + """A class for managing the temporal-host-info interface provider.""" + + def __init__(self, charm: CharmBase, port: int): + """Create a new instance of the TemporalHostInfoProvider class. + + :param: charm: The charm that is using this interface. + :type charm: CharmBase + :param: port: The port number to provide to requirers. This is typically + the 'frontend' service port. + :type port: int + """ + super().__init__(charm, 'host_info_provider') + self.charm = charm + self.port = port + charm.framework.observe( + charm.on[RELATION_NAME].relation_joined, self._on_host_info_relation_changed + ) + charm.framework.observe( + charm.on[RELATION_NAME].relation_changed, self._on_host_info_relation_changed + ) + charm.framework.observe(charm.on.leader_elected, self._on_config_changed) + charm.framework.observe(charm.on.config_changed, self._on_config_changed) + + def _on_host_info_relation_changed(self, event: RelationChangedEvent | RelationJoinedEvent): + """Update relation data. + + :param: event: The relation event that triggered this handler. + :type event: RelationChangedEvent | RelationJoinedEvent + """ + logger.info('Handling temporal-host-info relation event') + if self.charm.unit.is_leader() and 'frontend' in str(self.charm.config['services']): + host = str(self.charm.config['external-hostname']) + if binding := self.charm.model.get_binding(event.relation): + host = host or str(binding.network.bind_address) + event.relation.data[self.charm.app]['host'] = host + event.relation.data[self.charm.app]['port'] = str(self.port) + + def _on_config_changed(self, event: ConfigChangedEvent | LeaderElectedEvent): + """Update relation data on config change.""" + logger.info('Config changed, updating temporal-host-info relation data') + if self.charm.unit.is_leader() and 'frontend' in str(self.charm.config['services']): + host = str(self.charm.config['external-hostname']) + for relation in self.charm.model.relations.get('temporal-host-info', []): + if binding := self.charm.model.get_binding(relation): + host = host or str(binding.network.bind_address) + relation.data[self.charm.app]['host'] = host + relation.data[self.charm.app]['port'] = str(self.port) + + +class TemporalHostInfoRelationReadyEvent(EventBase): + """Event emitted when temporal-host-info relation is ready.""" + + def __init__( + self, + handle: Handle, + host: str, + port: int, + ): + super().__init__(handle) + self.host = host + self.port = port + + def snapshot(self) -> dict[str, str | int]: + """Return a snapshot of the event.""" + data = super().snapshot() + data.update({'host': self.host, 'port': self.port}) + return data + + def restore(self, snapshot: dict[str, str | int]) -> None: + """Restore the event from a snapshot.""" + super().restore(snapshot) + self.host = snapshot['host'] + self.port = snapshot['port'] + + +class TemporalHostInfoRequirerCharmEvents(ObjectEvents): + """List of events that the requirer charm can leverage.""" + + temporal_host_info_available = EventSource(TemporalHostInfoRelationReadyEvent) + + +class TemporalHostInfoRequirer(framework.Object): + """A class for managing the temporal-host-info interface requirer. + + Track this relation in your charm with: + + .. code-block:: python + + self.host_info = TemporalHostInfoRequirer(self) + # update container with new host info + framework.observe(self.host_info.on.temporal_host_info_available, self._update) + + def _update(self, event): + host = self.host_info.host + port = self.host_info.port + """ + + on = TemporalHostInfoRequirerCharmEvents() # type: ignore[reportAssignmentType] + + def __init__(self, charm: CharmBase): + """Create a new instance of the TemporalHostInfoProvider class. + + :param: charm: The charm that is using this interface. + :type charm: CharmBase + """ + super().__init__(charm, 'host_info_requirer') + self.charm = charm + charm.framework.observe( + charm.on[RELATION_NAME].relation_joined, self._on_host_info_relation_changed + ) + charm.framework.observe( + charm.on[RELATION_NAME].relation_changed, self._on_host_info_relation_changed + ) + + @property + def relations(self) -> list[Relation]: + """Return the relations for this interface.""" + return self.charm.model.relations.get(RELATION_NAME, []) + + @property + def host(self) -> str | None: + """Return the host from the relation data.""" + for relation in self.relations: + if relation and relation.app: + return relation.data[relation.app].get('host', None) + return None + + @property + def port(self) -> int | None: + """Return the port from the relation data.""" + for relation in self.relations: + if relation and relation.app: + port_str = relation.data[relation.app].get('port', None) + if port_str is not None: + return int(port_str) + return None + + def _on_host_info_relation_changed(self, event: RelationChangedEvent): + """Handle the relation joined/changed events. + + :param: event: The relation event that triggered this handler. + :type event: RelationChangedEvent | RelationJoinedEvent + """ + try: + host = event.relation.data[event.relation.app]['host'] + port = int(event.relation.data[event.relation.app]['port']) + except KeyError: + self.charm.unit.status = WaitingStatus('Waiting for temporal-host-info provider') + event.defer() + return + self.charm.unit.status = ActiveStatus() + self.on.temporal_host_info_available.emit(host=host, port=port) diff --git a/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/_version.py b/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/_version.py new file mode 100644 index 000000000..8c5a447db --- /dev/null +++ b/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/_version.py @@ -0,0 +1,15 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = '1.0.0' diff --git a/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/py.typed b/interfaces/temporal-host-info/src/charmlibs/interfaces/temporal_host_info/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/interfaces/temporal-host-info/tests/integration/charms/actions.yaml b/interfaces/temporal-host-info/tests/integration/charms/actions.yaml new file mode 100644 index 000000000..ea6507028 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/actions.yaml @@ -0,0 +1,4 @@ +# common actions.yaml file symlinked by these charms +# consider adding an action for each thing you want to test + +lib-version: diff --git a/interfaces/temporal-host-info/tests/integration/charms/common.py b/interfaces/temporal-host-info/tests/integration/charms/common.py new file mode 100644 index 000000000..21bd385c7 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/common.py @@ -0,0 +1,40 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common charm code for integration test charms. + +This file is symlinked alongside src/charm.py by these charms. +""" + +import logging + +import ops + +from charmlibs.interfaces import temporal_host_info + +logger = logging.getLogger(__name__) + + +class Charm(ops.CharmBase): + """Charm the application.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on['lib-version'].action, self._on_lib_version) + + def _on_lib_version(self, event: ops.ActionEvent): + logger.info('action [lib-version] called with params: %s', event.params) + results = {'version': temporal_host_info.__version__} + event.set_results(results) + logger.info('action [lib-version] set_results: %s', results) diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/actions.yaml b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/actions.yaml new file mode 120000 index 000000000..cfb67b5c9 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/actions.yaml @@ -0,0 +1 @@ +../../actions.yaml \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/charmcraft.yaml b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/charmcraft.yaml new file mode 100644 index 000000000..6ba136d99 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/charmcraft.yaml @@ -0,0 +1,41 @@ +# common charmcraft.yaml file symlinked by these charms +# k8s charms can define containers + resources in metadata.yaml + +name: temporal-host-info-provider +type: charm +summary: A small charm for use in integration tests. +description: A small charm for use in integration tests. + +base: ubuntu@24.04 +platforms: + amd64: + +parts: + charm: + source: . + plugin: uv + build-snaps: [astral-uv] + +containers: + workload: + resource: workload + +resources: + workload: + type: oci-image + description: OCI image for the 'workload' container. + upstream-source: ghcr.io/canonical/ubuntu-noble:latest + +provides: + temporal-host-info: + interface: temporal-host-info +config: + options: + services: + type: string + default: frontend + description: The Temporal services to run. + external-hostname: + type: string + default: "" + description: The external hostname to use. diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/README.md b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/README.md new file mode 120000 index 000000000..8addd38ae --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/README.md @@ -0,0 +1 @@ +../../../../../../README.md \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/pyproject.toml b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/pyproject.toml new file mode 120000 index 000000000..d2c52e649 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/pyproject.toml @@ -0,0 +1 @@ +../../../../../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/src b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/src new file mode 120000 index 000000000..63fbb1dd4 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/library/src @@ -0,0 +1 @@ +../../../../../../src \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/pyproject.toml b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/pyproject.toml new file mode 120000 index 000000000..00c904eb8 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/pyproject.toml @@ -0,0 +1 @@ +../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/src/charm.py b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/src/charm.py new file mode 100644 index 000000000..6d2e59b3f --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/src/charm.py @@ -0,0 +1,45 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""K8s charm for testing.""" + +import logging + +import common +import ops + +from charmlibs.interfaces import temporal_host_info + +logger = logging.getLogger(__name__) + +CONTAINER = 'workload' + + +class Charm(common.Charm): + """Charm the application.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on[CONTAINER].pebble_ready, self._on_pebble_ready) + self.temporal_host_info_provider = temporal_host_info.TemporalHostInfoProvider( + self, port=7233 + ) + + def _on_pebble_ready(self, event: ops.PebbleReadyEvent): + """Handle pebble-ready event.""" + self.unit.status = ops.ActiveStatus() + + +if __name__ == '__main__': # pragma: nocover + ops.main(Charm) diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/src/common.py b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/src/common.py new file mode 120000 index 000000000..00a8a48b4 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/provider/src/common.py @@ -0,0 +1 @@ +../../../common.py \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/pyproject.toml b/interfaces/temporal-host-info/tests/integration/charms/k8s/pyproject.toml new file mode 100644 index 000000000..6e3dc8355 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/pyproject.toml @@ -0,0 +1,14 @@ +# common pyproject.toml file symlinked by these charms + +[project] +name = "integration-test-charm" +version = "0.0.0.dev0" +description = "Charm for integration tests." +requires-python = ">=3.10" +dependencies = [ + "ops==2.21.1", + "charmlibs-interfaces-temporal-host-info", +] + +[tool.uv.sources] +"charmlibs-interfaces-temporal-host-info" = { path = "library", editable = true } diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/actions.yaml b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/actions.yaml new file mode 120000 index 000000000..cfb67b5c9 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/actions.yaml @@ -0,0 +1 @@ +../../actions.yaml \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/charmcraft.yaml b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/charmcraft.yaml new file mode 100644 index 000000000..4c230e59c --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/charmcraft.yaml @@ -0,0 +1,31 @@ +# common charmcraft.yaml file symlinked by these charms +# k8s charms can define containers + resources in metadata.yaml + +name: temporal-host-info-requirer +type: charm +summary: A small charm for use in integration tests. +description: A small charm for use in integration tests. + +base: ubuntu@24.04 +platforms: + amd64: + +parts: + charm: + source: . + plugin: uv + build-snaps: [astral-uv] + +containers: + workload: + resource: workload + +resources: + workload: + type: oci-image + description: OCI image for the 'workload' container. + upstream-source: ghcr.io/canonical/ubuntu-noble:latest + +requires: + temporal-host-info: + interface: temporal-host-info diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/README.md b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/README.md new file mode 120000 index 000000000..8addd38ae --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/README.md @@ -0,0 +1 @@ +../../../../../../README.md \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/pyproject.toml b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/pyproject.toml new file mode 120000 index 000000000..d2c52e649 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/pyproject.toml @@ -0,0 +1 @@ +../../../../../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/src b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/src new file mode 120000 index 000000000..63fbb1dd4 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/library/src @@ -0,0 +1 @@ +../../../../../../src \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/pyproject.toml b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/pyproject.toml new file mode 120000 index 000000000..00c904eb8 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/pyproject.toml @@ -0,0 +1 @@ +../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/src/charm.py b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/src/charm.py new file mode 100644 index 000000000..19bc8d318 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/src/charm.py @@ -0,0 +1,48 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""K8s charm for testing.""" + +import logging + +import common +import ops + +from charmlibs.interfaces import temporal_host_info + +logger = logging.getLogger(__name__) + +CONTAINER = 'workload' + + +class Charm(common.Charm): + """Charm the application.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on[CONTAINER].pebble_ready, self._configure) + self.host_info = temporal_host_info.TemporalHostInfoRequirer(self) + framework.observe(self.host_info.on.temporal_host_info_available, self._configure) + + def _configure(self, event: ops.EventBase): + if self.host_info.host is None or self.host_info.port is None: + self.unit.status = ops.ActiveStatus('Waiting for temporal-host-info relation data') + return + self.unit.status = ops.ActiveStatus( + f'Temporal host: {self.host_info.host}, port: {self.host_info.port}' + ) + + +if __name__ == '__main__': # pragma: nocover + ops.main(Charm) diff --git a/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/src/common.py b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/src/common.py new file mode 120000 index 000000000..00a8a48b4 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/k8s/requirer/src/common.py @@ -0,0 +1 @@ +../../../common.py \ No newline at end of file diff --git a/interfaces/temporal-host-info/tests/integration/charms/pyproject.toml b/interfaces/temporal-host-info/tests/integration/charms/pyproject.toml new file mode 100644 index 000000000..6e3dc8355 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/charms/pyproject.toml @@ -0,0 +1,14 @@ +# common pyproject.toml file symlinked by these charms + +[project] +name = "integration-test-charm" +version = "0.0.0.dev0" +description = "Charm for integration tests." +requires-python = ">=3.10" +dependencies = [ + "ops==2.21.1", + "charmlibs-interfaces-temporal-host-info", +] + +[tool.uv.sources] +"charmlibs-interfaces-temporal-host-info" = { path = "library", editable = true } diff --git a/interfaces/temporal-host-info/tests/integration/conftest.py b/interfaces/temporal-host-info/tests/integration/conftest.py new file mode 100644 index 000000000..d8a152385 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/conftest.py @@ -0,0 +1,80 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixtures for Juju integration tests.""" + +import logging +import os +import pathlib +import sys +import time +import typing +from collections.abc import Iterator + +import jubilant +import pytest + +logger = logging.getLogger(__name__) + + +def pytest_addoption(parser: pytest.OptionGroup): + parser.addoption( + '--keep-models', + action='store_true', + default=False, + help='keep temporarily-created models', + ) + + +@pytest.fixture(scope='session') +def provider() -> str: + return 'temporal-host-info-provider' + + +@pytest.fixture(scope='session') +def requirer() -> str: + return 'temporal-host-info-requirer' + + +@pytest.fixture(scope='module') +def juju(request: pytest.FixtureRequest, provider: str, requirer: str) -> Iterator[jubilant.Juju]: + """Pytest fixture that wraps :meth:`jubilant.with_model`. + + This adds command line parameter ``--keep-models`` (see help for details). + """ + keep_models = typing.cast('bool', request.config.getoption('--keep-models')) + with jubilant.temp_model(keep=keep_models) as juju: + juju.model_config({'logging-config': '=INFO;unit=DEBUG'}) + _deploy(juju) + juju.wait(jubilant.all_active) + yield juju + if request.session.testsfailed: + logger.info('Collecting Juju logs ...') + time.sleep(0.5) # Wait for Juju to process logs. + log = juju.debug_log(limit=1000) + print(log, end='', file=sys.stderr) + + +def _deploy(juju: jubilant.Juju) -> None: + substrate = os.environ['CHARMLIBS_SUBSTRATE'] + # tag = os.environ.get('CHARMLIBS_TAG', '') # get the tag if needed + paths = [ + pathlib.Path(__file__).parent / '.packed' / 'provider' / f'{substrate}.charm', + pathlib.Path(__file__).parent / '.packed' / 'requirer' / f'{substrate}.charm', + ] + for path in paths: + if substrate == 'k8s': + juju.deploy(path, resources={'workload': 'ubuntu:latest'}) # name set in metadata.yaml + else: + juju.deploy(path) diff --git a/interfaces/temporal-host-info/tests/integration/pack.sh b/interfaces/temporal-host-info/tests/integration/pack.sh new file mode 100755 index 000000000..c37d2a288 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/pack.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# This script is executed in this directory via `just pack-k8s` or `just pack-machine`. +# Extra args are passed to this script, e.g. `just pack-k8s foo` -> $1 is 'foo'. +# In CI, the `just pack-` commands are invoked: +# - If this file exists and `just integration-` would execute any tests +# - Before running integration tests +# - With no additional arguments +# +# Environment variables: +# $CHARMLIBS_SUBSTRATE will have the value 'k8s' or 'machine' (set by pack-k8s or pack-machine) +# In CI, $CHARMLIBS_TAG is set based on pyproject.toml:tool.charmlibs.integration.tags +# For local testing, set $CHARMLIBS_TAG directly or use the tag variable. For example: +# just tag=24.04 pack-k8s some extra args +set -xueo pipefail + +TMP_DIR=".tmp" # clean temporary directory where charms will be packed +PACKED_DIR=".packed" # where packed charms will be placed with name expected in conftest.py + +: copy charm files to temporary directory for packing, dereferencing symlinks +rm -rf "$TMP_DIR" +cp --recursive --dereference "charms/$CHARMLIBS_SUBSTRATE/" "$TMP_DIR" + +: pack charm +cd "$TMP_DIR" +cd provider +uv lock # required by uv charm plugin +charmcraft pack +cd .. +cd requirer +uv lock # required by uv charm plugin +charmcraft pack +cd ../.. + +: place packed charm in expected location +mkdir -p "$PACKED_DIR" +mkdir -p "$PACKED_DIR/provider/" +mkdir -p "$PACKED_DIR/requirer/" +mv "$TMP_DIR"/provider/*.charm "$PACKED_DIR/provider/$CHARMLIBS_SUBSTRATE.charm" # read by conftest.py +mv "$TMP_DIR"/requirer/*.charm "$PACKED_DIR/requirer/$CHARMLIBS_SUBSTRATE.charm" # read by conftest.py diff --git a/interfaces/temporal-host-info/tests/integration/test_temporal_host_info.py b/interfaces/temporal-host-info/tests/integration/test_temporal_host_info.py new file mode 100644 index 000000000..774111ec0 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/test_temporal_host_info.py @@ -0,0 +1,46 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import jubilant + + +def test_deploy(juju: jubilant.Juju, provider: str, requirer: str): + """The deployment takes place in the module scoped `juju` fixture.""" + assert provider in juju.status().apps + assert requirer in juju.status().apps + + +def test_integrate(juju: jubilant.Juju, provider: str, requirer: str): + """Test that the relation is established.""" + hostname = 'temporal.server.local' + juju.config(provider, {'external-hostname': hostname}) + juju.integrate(f'{provider}:temporal-host-info', f'{requirer}:temporal-host-info') + juju.wait(jubilant.all_active) + status = juju.status() + assert ( + status.apps[requirer].units[f'{requirer}/0'].workload_status.message + == f'Temporal host: {hostname}, port: 7233' + ) + + +def test_config_changed(juju: jubilant.Juju, provider: str, requirer: str): + """Test that changing the provider config updates the requirer status.""" + new_hostname = 'new.temporal.server.local' + juju.config(provider, {'external-hostname': new_hostname}) + juju.wait(jubilant.all_active) + status = juju.status() + assert ( + status.apps[requirer].units[f'{requirer}/0'].workload_status.message + == f'Temporal host: {new_hostname}, port: 7233' + ) diff --git a/interfaces/temporal-host-info/tests/integration/test_version.py b/interfaces/temporal-host-info/tests/integration/test_version.py new file mode 100644 index 000000000..7dd975e68 --- /dev/null +++ b/interfaces/temporal-host-info/tests/integration/test_version.py @@ -0,0 +1,26 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Integration tests using real Juju and pre-packed charm(s).""" + +import jubilant + +from charmlibs.interfaces import temporal_host_info + + +def test_lib_version(juju: jubilant.Juju, provider: str, requirer: str): + result = juju.run(f'{provider}/0', 'lib-version') + assert result.results['version'] == temporal_host_info.__version__ + result = juju.run(f'{requirer}/0', 'lib-version') + assert result.results['version'] == temporal_host_info.__version__ diff --git a/interfaces/temporal-host-info/tests/unit/conftest.py b/interfaces/temporal-host-info/tests/unit/conftest.py new file mode 100644 index 000000000..ee259deed --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/conftest.py @@ -0,0 +1,15 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixtures for unit tests, typically mocking out parts of the external system.""" diff --git a/interfaces/temporal-host-info/tests/unit/dummy_provider/charmcraft.yaml b/interfaces/temporal-host-info/tests/unit/dummy_provider/charmcraft.yaml new file mode 100644 index 000000000..2a8f45f13 --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/dummy_provider/charmcraft.yaml @@ -0,0 +1,35 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: temporal-host-info-interface-provider + +description: | + Dummy temporal-host-info-interface provider. +summary: | + Dummy temporal-host-info-interface provider. + +provides: + temporal-host-info: + interface: temporal-host-info +config: + options: + services: + type: string + default: frontend + description: The Temporal services to run. + external-hostname: + type: string + default: "" + description: The external hostname to use. diff --git a/interfaces/temporal-host-info/tests/unit/dummy_provider/src/charm.py b/interfaces/temporal-host-info/tests/unit/dummy_provider/src/charm.py new file mode 100644 index 000000000..77f34c472 --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/dummy_provider/src/charm.py @@ -0,0 +1,30 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +from ops.charm import CharmBase +from ops.main import main + +from charmlibs.interfaces.temporal_host_info import TemporalHostInfoProvider + + +class DummyHostInfoProviderCharm(CharmBase): + def __init__(self, *args: Any): + super().__init__(*args) + self.host_info = TemporalHostInfoProvider(self, port=7233) + + +if __name__ == '__main__': + main(DummyHostInfoProviderCharm) diff --git a/interfaces/temporal-host-info/tests/unit/dummy_requirer/charmcraft.yaml b/interfaces/temporal-host-info/tests/unit/dummy_requirer/charmcraft.yaml new file mode 100644 index 000000000..d0820604d --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/dummy_requirer/charmcraft.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: temporal-host-info-interface-requirer + +description: | + Dummy temporal-host-info-interface requirer. +summary: | + Dummy temporal-host-info-interface requirer. + +requires: + temporal-host-info: + interface: temporal-host-info diff --git a/interfaces/temporal-host-info/tests/unit/dummy_requirer/src/charm.py b/interfaces/temporal-host-info/tests/unit/dummy_requirer/src/charm.py new file mode 100644 index 000000000..eaa6297bc --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/dummy_requirer/src/charm.py @@ -0,0 +1,39 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +from ops.charm import CharmBase +from ops.main import main +from scenario import ActiveStatus + +from charmlibs.interfaces.temporal_host_info import TemporalHostInfoRequirer + + +class DummyHostInfoRequirerCharm(CharmBase): + def __init__(self, *args: Any): + super().__init__(*args) + self.host_info = TemporalHostInfoRequirer(self) + self.framework.observe( + self.host_info.on.temporal_host_info_available, self._on_host_info_available + ) + + def _on_host_info_available(self, event: Any) -> None: + host = self.host_info.host + port = self.host_info.port + self.unit.status = ActiveStatus(f'Host: {host}, Port: {port}') + + +if __name__ == '__main__': + main(DummyHostInfoRequirerCharm) diff --git a/interfaces/temporal-host-info/tests/unit/test_provides.py b/interfaces/temporal-host-info/tests/unit/test_provides.py new file mode 100644 index 000000000..9013210d9 --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/test_provides.py @@ -0,0 +1,81 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import Any + +import pytest +import yaml +from ops import testing + +from dummy_provider.src.charm import DummyHostInfoProviderCharm + +METADATA: dict[str, Any] = yaml.safe_load( + (Path(__file__).parent / 'dummy_provider' / 'charmcraft.yaml').read_text() +) + + +class TestTemporalHostInfoProvider: + @pytest.fixture(autouse=True) + def context(self): + self.ctx = testing.Context( + charm_type=DummyHostInfoProviderCharm, meta=METADATA, config=METADATA['config'] + ) + + def test_provides(self): + relation = testing.Relation( + endpoint='temporal-host-info', + interface='temporal-host-info', + remote_app_name='temporal-host-info-interface-requirer', + ) + state_in = testing.State( + relations={relation}, + config={'services': 'frontend', 'external-hostname': 'test-host.example.com'}, + leader=True, + ) + state_out = self.ctx.run(self.ctx.on.relation_changed(relation), state_in) + for r in state_out.relations: + if r.id == relation.id: + assert r.local_app_data == { + 'host': 'test-host.example.com', + 'port': '7233', + } + + def test_provides_config_changed(self): + relation = testing.Relation( + endpoint='temporal-host-info', + interface='temporal-host-info', + remote_app_name='temporal-host-info-interface-requirer', + ) + state_in = testing.State( + relations={relation}, + config={'services': 'frontend', 'external-hostname': 'initial-host.example.com'}, + leader=True, + ) + # Initial relation changed to set data + state_intermediate = self.ctx.run(self.ctx.on.relation_changed(relation), state_in) + # Now change config + state_updated = testing.State( + relations=state_intermediate.relations, + config={'services': 'frontend', 'external-hostname': 'updated-host.example.com'}, + leader=True, + ) + + state_out = self.ctx.run(self.ctx.on.config_changed(), state_updated) + for r in state_out.relations: + if r.id == relation.id: + assert r.local_app_data == { + 'host': 'updated-host.example.com', + 'port': '7233', + } diff --git a/interfaces/temporal-host-info/tests/unit/test_requires.py b/interfaces/temporal-host-info/tests/unit/test_requires.py new file mode 100644 index 000000000..8cc1d0e3a --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/test_requires.py @@ -0,0 +1,51 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import Any + +import pytest +import yaml +from ops import testing + +from dummy_requirer.src.charm import DummyHostInfoRequirerCharm + +METADATA: dict[str, Any] = yaml.safe_load( + (Path(__file__).parent / 'dummy_requirer' / 'charmcraft.yaml').read_text() +) + + +class TestTemporalHostInfoRequirer: + @pytest.fixture(autouse=True) + def context(self): + self.ctx = testing.Context( + charm_type=DummyHostInfoRequirerCharm, + meta=METADATA, + ) + + def test_require(self): + relation = testing.Relation( + endpoint='temporal-host-info', + interface='temporal-host-info', + remote_app_name='temporal-host-info-interface-provider', + remote_app_data={ + 'host': 'test-host.example.com', + 'port': '7233', + }, + ) + state_in = testing.State(relations={relation}) + state_out = self.ctx.run(self.ctx.on.relation_changed(relation), state_in) + assert state_out.unit_status == testing.ActiveStatus( + 'Host: test-host.example.com, Port: 7233' + ) diff --git a/interfaces/temporal-host-info/tests/unit/test_version.py b/interfaces/temporal-host-info/tests/unit/test_version.py new file mode 100644 index 000000000..7d1311d15 --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/test_version.py @@ -0,0 +1,21 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for library code, not involving charm code.""" + +from charmlibs.interfaces import temporal_host_info + + +def test_version(): + assert isinstance(temporal_host_info.__version__, str) diff --git a/interfaces/temporal-host-info/tests/unit/test_version_in_charm.py b/interfaces/temporal-host-info/tests/unit/test_version_in_charm.py new file mode 100644 index 000000000..20e596057 --- /dev/null +++ b/interfaces/temporal-host-info/tests/unit/test_version_in_charm.py @@ -0,0 +1,38 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Light weight state-transition tests of the library in a charming context.""" + +import ops +import ops.testing + +from charmlibs.interfaces import temporal_host_info + + +class Charm(ops.CharmBase): + package_version: str + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on.start, self._on_start) + + def _on_start(self, event: ops.StartEvent): + self.package_version = temporal_host_info.__version__ + + +def test_version(): + ctx = ops.testing.Context(Charm, meta={'name': 'charm'}) + with ctx(ctx.on.start(), ops.testing.State()) as manager: + manager.run() + assert isinstance(manager.charm.package_version, str) diff --git a/interfaces/temporal-host-info/uv.lock b/interfaces/temporal-host-info/uv.lock new file mode 100644 index 000000000..2d5fe4f65 --- /dev/null +++ b/interfaces/temporal-host-info/uv.lock @@ -0,0 +1,192 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "charmlibs-interfaces-temporal-host-info" +source = { editable = "." } +dependencies = [ + { name = "jubilant" }, + { name = "ops" }, +] + +[package.dev-dependencies] +integration = [ + { name = "jubilant" }, +] +unit = [ + { name = "ops", extra = ["testing"] }, +] + +[package.metadata] +requires-dist = [ + { name = "jubilant", specifier = ">=1.5.0" }, + { name = "ops", specifier = "==2.21.1" }, +] + +[package.metadata.requires-dev] +functional = [] +integration = [{ name = "jubilant" }] +lint = [] +unit = [{ name = "ops", extras = ["testing"] }] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "jubilant" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/af/2c7d2a677389eb4de3bb841f399b749ca2fd4c6c1b70313e21249536e6be/jubilant-1.5.0.tar.gz", hash = "sha256:055c65a662586191939a1a3d3e2b6d08b71ecf0c7f403a1c7ba0cde6ecaf3bbd", size = 28433, upload-time = "2025-10-10T01:08:06.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/c9/05e5aa65baa7b71b17c1314b8f25744f36fd9aad60ef9fb46fd84347885a/jubilant-1.5.0-py3-none-any.whl", hash = "sha256:eec58340b9d3d478f31e18e33281638fdf0d9b84608c8935368f7a1bb1972255", size = 28275, upload-time = "2025-10-10T01:08:04.963Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + +[[package]] +name = "ops" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "opentelemetry-api" }, + { name = "pyyaml" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/c5/f0098a9b1b72680b3682043227a628a08a7b5b9592fc98ea6efa0d638017/ops-2.21.1.tar.gz", hash = "sha256:4a8190420813ba37e7a0399d656008f99c79015d7f72e138bad7cb1ac403d0b0", size = 496427, upload-time = "2025-05-01T03:03:23.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/c7/b70271ee12418144d5c596f37745c21da105470d365d834a9fce071f7bc2/ops-2.21.1-py3-none-any.whl", hash = "sha256:6745084c8e73bc485c254f95664bd85ddcf786c91b90782f2830c8333a1440b2", size = 182682, upload-time = "2025-05-01T03:03:20.946Z" }, +] + +[package.optional-dependencies] +testing = [ + { name = "ops-scenario" }, +] + +[[package]] +name = "ops-scenario" +version = "7.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ops" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/95/90312165f3e1d5876872ad64131be9c0f26b5aa09bd73a436a21b0752820/ops_scenario-7.21.1.tar.gz", hash = "sha256:534b407b34212161c3e74cb396b79ca7449932e483053e924146fdf0140876b9", size = 141631, upload-time = "2025-05-01T03:03:30.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/3b/4cc28ea9d77015ecf8fc5c3d362bb274e35ffaa2e6fbd87909a5384afa40/ops_scenario-7.21.1-py3-none-any.whl", hash = "sha256:eeb9def2c31a1db2429cf21bd9b8cbd2ac092cc1fbe5591e850e4aa192c542fa", size = 72451, upload-time = "2025-05-01T03:03:28.488Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/interfaces/temporal-worker-consumer/CHANGELOG.md b/interfaces/temporal-worker-consumer/CHANGELOG.md new file mode 100644 index 000000000..ba9d0ef08 --- /dev/null +++ b/interfaces/temporal-worker-consumer/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 - 25 November 2025 + +Initial release. diff --git a/interfaces/temporal-worker-consumer/README.md b/interfaces/temporal-worker-consumer/README.md new file mode 100644 index 000000000..5de5ae704 --- /dev/null +++ b/interfaces/temporal-worker-consumer/README.md @@ -0,0 +1,11 @@ +# charmlibs.interfaces.temporal_worker_consumer + +The `temporal-worker-consumer` interface library. + +To install, add `charmlibs-interfaces-temporal-worker-consumer` to your Python dependencies. Then in your Python code, import as: + +```py +from charmlibs.interfaces import temporal_worker_consumer +``` + +See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/temporal_worker_consumer) for more. diff --git a/interfaces/temporal-worker-consumer/pyproject.toml b/interfaces/temporal-worker-consumer/pyproject.toml new file mode 100644 index 000000000..b0e47c752 --- /dev/null +++ b/interfaces/temporal-worker-consumer/pyproject.toml @@ -0,0 +1,74 @@ +[project] +name = "charmlibs-interfaces-temporal-worker-consumer" +description = "The charmlibs.interfaces.temporal_worker_consumer package." +readme = "README.md" +requires-python = ">=3.10" +authors = [ + {name="The Charm Engineering team at Canonical"}, +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Development Status :: 5 - Production/Stable", +] +dynamic = ["version"] +dependencies = [ + "jubilant>=1.5.0", + # "ops", + "ops==2.21.1", + "pytest>=8.3.5", +] + +[dependency-groups] +lint = [ # installed for `just lint interfaces/temporal-worker-consumer` (unit, functional, and integration are also installed) + # "typing_extensions", +] +unit = [ # installed for `just unit interfaces/temporal-worker-consumer` + "ops[testing]", +] +functional = [ # installed for `just functional interfaces/temporal-worker-consumer` +] +integration = [ # installed for `just integration interfaces/temporal-worker-consumer` + "jubilant", +] + +[project.urls] +"Repository" = "https://github.com/canonical/charmlibs" +"Issues" = "https://github.com/canonical/charmlibs/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/charmlibs"] + +[tool.hatch.version] +path = "src/charmlibs/interfaces/temporal_worker_consumer/_version.py" + +[tool.ruff] +extend = "../../pyproject.toml" +src = ["src", "tests/unit", "tests/functional", "tests/integration"] # correctly sort local imports in tests + +[tool.ruff.lint.extend-per-file-ignores] +# add additional per-file-ignores here to avoid overriding repo-level config +"tests/**/*" = [ + # "E501", # line too long +] + +[tool.pyright] +extends = "../../pyproject.toml" +include = ["src", "tests"] +pythonVersion = "3.10" # check no python > 3.10 features are used + +[tool.charmlibs.functional] +ubuntu = [] # ubuntu versions to run functional tests with, e.g. "24.04" (defaults to just "latest") +pebble = [] # pebble versions to run functional tests with, e.g. "v1.0.0", "master" (defaults to no pebble versions) +sudo = false # whether to run functional tests with sudo (defaults to false) + +[tool.charmlibs.integration] +# tags to run integration tests with (defaults to running once with no tag, i.e. tags = ['']) +# Available in CI in tests/integration/pack.sh and integration tests as CHARMLIBS_TAG +tags = [] # Not used by the pack.sh and integration tests generated by the template diff --git a/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/__init__.py b/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/__init__.py new file mode 100644 index 000000000..ee2948fe8 --- /dev/null +++ b/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Relation management for temporal-worker-consumer interface.""" + +from ._temporal_worker_consumer import ( + TemporalWorkerConsumerProvider, + TemporalWorkerConsumerRequirer, +) +from ._version import __version__ as __version__ + +__all__ = [ + 'TemporalWorkerConsumerProvider', + 'TemporalWorkerConsumerRequirer', +] diff --git a/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/_temporal_worker_consumer.py b/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/_temporal_worker_consumer.py new file mode 100644 index 000000000..146b3aff9 --- /dev/null +++ b/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/_temporal_worker_consumer.py @@ -0,0 +1,170 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Relation management for temporal-worker-consumer interface.""" + +import logging + +from ops import ConfigChangedEvent, Handle, RelationChangedEvent, RelationJoinedEvent, framework +from ops.charm import CharmBase +from ops.framework import EventBase, EventSource, ObjectEvents +from ops.model import ActiveStatus, Relation, WaitingStatus + +logger = logging.getLogger(__name__) + +RELATION_NAME = 'temporal-worker-consumer' + + +class TemporalWorkerConsumerProvider(framework.Object): + """A class for managing the temporal-worker-consumer interface provider.""" + + def __init__(self, charm: CharmBase): + """Create a new instance of the TemporalWorkerConsumerProvider class. + + :param: charm: The charm that is using this interface. + """ + super().__init__(charm, 'worker-consumer-provider') + self.charm = charm + + charm.framework.observe( + charm.on[RELATION_NAME].relation_joined, self._on_worker_consumer_changed + ) + charm.framework.observe( + charm.on[RELATION_NAME].relation_changed, self._on_worker_consumer_changed + ) + charm.framework.observe(charm.on.config_changed, self._on_config_changed) + + def _on_worker_consumer_changed(self, event: RelationChangedEvent | RelationJoinedEvent): + """Update relation data. + + :param: event: The relation event that triggered this handler. + :type event: RelationChangedEvent | RelationJoinedEvent + """ + logger.info('Handling temporal-worker-consumer relation event') + if self.charm.unit.is_leader(): + event.relation.data[self.charm.app]['namespace'] = str(self.charm.config['namespace']) + event.relation.data[self.charm.app]['queue'] = str(self.charm.config['queue']) + + def _on_config_changed(self, event: ConfigChangedEvent): + """Handle config changes by updating relation data. + + :param: event: The config changed event that triggered this handler. + :type event: ConfigChangedEvent + """ + logger.info('Config changed, updating temporal-worker-consumer relation data') + if self.charm.unit.is_leader(): + # Config could have changed, so update all relations + for relation in self.charm.model.relations.get(RELATION_NAME, ()): + relation.data[self.charm.app]['namespace'] = str(self.charm.config['namespace']) + relation.data[self.charm.app]['queue'] = str(self.charm.config['queue']) + + +class TemporalWorkerConsumerRelationReadyEvent(EventBase): + """Event emitted when worker-consumer relation is ready.""" + + def __init__( + self, + handle: Handle, + namespace: str, + queue: str, + ): + super().__init__(handle) + self.namespace = namespace + self.queue = queue + + def snapshot(self) -> dict[str, str]: + """Return a snapshot of the event.""" + return {'namespace': self.namespace, 'queue': self.queue} + + def restore(self, snapshot: dict[str, str]) -> None: + """Restore the event from a snapshot.""" + self.namespace = snapshot['namespace'] + self.queue = snapshot['queue'] + + +class TemporalWorkerConsumerRequirerCharmEvents(ObjectEvents): + """List of events that the worker-consumer requirer charm can leverage.""" + + temporal_worker_consumer_available = EventSource(TemporalWorkerConsumerRelationReadyEvent) + + +class TemporalWorkerConsumerRequirer(framework.Object): + """A class for managing the temporal-worker-consumer interface requirer. + + Track this relation in your charm with: + + .. code-block:: python + + self.worker_consumer = TemporalWorkerConsumerRequirer(self) + # update container with new worker consumer info + framework.observe(self.worker_consumer.on.temporal_worker_consumer_available, self._update) + + def _update(self, event): + namespace = self.worker_consumer.namespace + queue = self.worker_consumer.queue + """ + + on = TemporalWorkerConsumerRequirerCharmEvents() # type: ignore[reportAssignmentType] + + def __init__(self, charm: CharmBase): + """Create a new instance of the TemporalWorkerConsumerRequirer class. + + :param: charm: The charm that is using this interface. + """ + super().__init__(charm, 'worker-consumer-requirer') + self.charm = charm + charm.framework.observe( + charm.on[RELATION_NAME].relation_joined, self._on_worker_consumer_relation_changed + ) + charm.framework.observe( + charm.on[RELATION_NAME].relation_changed, self._on_worker_consumer_relation_changed + ) + + @property + def relations(self) -> list[Relation]: + """Return the relations for this interface.""" + return self.charm.model.relations.get(RELATION_NAME, []) + + @property + def namespace(self) -> str | None: + """Return the namespace from the relation data.""" + for relation in self.relations: + if relation and relation.app: + return relation.data[relation.app].get('namespace', None) + return None + + @property + def queue(self) -> str | None: + """Return the queue from the relation data.""" + for relation in self.relations: + if relation and relation.app: + return relation.data[relation.app].get('queue', None) + return None + + def _on_worker_consumer_relation_changed(self, event: RelationChangedEvent): + """Retrieve relation data and emit worker_consumer_available event. + + :param: event: The relation event that triggered this handler. + :type event: RelationChangedEvent | RelationJoinedEvent + """ + try: + namespace = event.relation.data[event.relation.app]['namespace'] + queue = event.relation.data[event.relation.app]['queue'] + except KeyError: + self.charm.unit.status = WaitingStatus('Waiting for temporal-worker-consumer provider') + event.defer() + return + self.charm.unit.status = ActiveStatus() + self.on.temporal_worker_consumer_available.emit(namespace=namespace, queue=queue) diff --git a/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/_version.py b/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/_version.py new file mode 100644 index 000000000..8c5a447db --- /dev/null +++ b/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/_version.py @@ -0,0 +1,15 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = '1.0.0' diff --git a/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/py.typed b/interfaces/temporal-worker-consumer/src/charmlibs/interfaces/temporal_worker_consumer/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/actions.yaml b/interfaces/temporal-worker-consumer/tests/integration/charms/actions.yaml new file mode 100644 index 000000000..ea6507028 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/actions.yaml @@ -0,0 +1,4 @@ +# common actions.yaml file symlinked by these charms +# consider adding an action for each thing you want to test + +lib-version: diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/common.py b/interfaces/temporal-worker-consumer/tests/integration/charms/common.py new file mode 100644 index 000000000..b84475d11 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/common.py @@ -0,0 +1,40 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common charm code for integration test charms. + +This file is symlinked alongside src/charm.py by these charms. +""" + +import logging + +import ops + +from charmlibs.interfaces import temporal_worker_consumer + +logger = logging.getLogger(__name__) + + +class Charm(ops.CharmBase): + """Charm the application.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on['lib-version'].action, self._on_lib_version) + + def _on_lib_version(self, event: ops.ActionEvent): + logger.info('action [lib-version] called with params: %s', event.params) + results = {'version': temporal_worker_consumer.__version__} + event.set_results(results) + logger.info('action [lib-version] set_results: %s', results) diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/README.md b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/README.md new file mode 120000 index 000000000..1dfab2425 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/README.md @@ -0,0 +1 @@ +../../../../../README.md \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/pyproject.toml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/pyproject.toml new file mode 120000 index 000000000..be00ff53f --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/pyproject.toml @@ -0,0 +1 @@ +../../../../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/src b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/src new file mode 120000 index 000000000..d753b57a1 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/library/src @@ -0,0 +1 @@ +../../../../../src \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/actions.yaml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/actions.yaml new file mode 120000 index 000000000..cfb67b5c9 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/actions.yaml @@ -0,0 +1 @@ +../../actions.yaml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/charmcraft.yaml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/charmcraft.yaml new file mode 100644 index 000000000..92b869544 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/charmcraft.yaml @@ -0,0 +1,41 @@ +# common charmcraft.yaml file symlinked by these charms +# k8s charms can define containers + resources in metadata.yaml + +name: temporal-worker-consumer-provider +type: charm +summary: A small charm for use in integration tests. +description: A small charm for use in integration tests. + +base: ubuntu@24.04 +platforms: + amd64: + +parts: + charm: + source: . + plugin: uv + build-snaps: [astral-uv] + +containers: + workload: + resource: workload + +resources: + workload: + type: oci-image + description: OCI image for the 'workload' container. + upstream-source: ghcr.io/canonical/ubuntu-noble:latest + +provides: + temporal-worker-consumer: + interface: temporal-worker-consumer +config: + options: + queue: + type: string + default: default-queue + description: The Temporal task queue to use. + namespace: + type: string + default: default-namespace + description: The Temporal namespace to use. diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/README.md b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/README.md new file mode 120000 index 000000000..8addd38ae --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/README.md @@ -0,0 +1 @@ +../../../../../../README.md \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/pyproject.toml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/pyproject.toml new file mode 120000 index 000000000..d2c52e649 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/pyproject.toml @@ -0,0 +1 @@ +../../../../../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/src b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/src new file mode 120000 index 000000000..63fbb1dd4 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/library/src @@ -0,0 +1 @@ +../../../../../../src \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/pyproject.toml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/pyproject.toml new file mode 120000 index 000000000..00c904eb8 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/pyproject.toml @@ -0,0 +1 @@ +../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/src/charm.py b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/src/charm.py new file mode 100644 index 000000000..d8e1a90e5 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/src/charm.py @@ -0,0 +1,47 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""K8s charm for testing.""" + +import logging + +import common +import ops + +from charmlibs.interfaces import temporal_worker_consumer + +logger = logging.getLogger(__name__) + +CONTAINER = 'workload' + + +class Charm(common.Charm): + """Charm the application.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on[CONTAINER].pebble_ready, self._on_pebble_ready) + self.temporal_worker_consumer_provider = ( + temporal_worker_consumer.TemporalWorkerConsumerProvider( + self, + ) + ) + + def _on_pebble_ready(self, event: ops.PebbleReadyEvent): + """Handle pebble-ready event.""" + self.unit.status = ops.ActiveStatus() + + +if __name__ == '__main__': # pragma: nocover + ops.main(Charm) diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/src/common.py b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/src/common.py new file mode 120000 index 000000000..00a8a48b4 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/provider/src/common.py @@ -0,0 +1 @@ +../../../common.py \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/pyproject.toml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/pyproject.toml new file mode 120000 index 000000000..1e11d7825 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/pyproject.toml @@ -0,0 +1 @@ +../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/actions.yaml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/actions.yaml new file mode 120000 index 000000000..cfb67b5c9 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/actions.yaml @@ -0,0 +1 @@ +../../actions.yaml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/charmcraft.yaml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/charmcraft.yaml new file mode 100644 index 000000000..0e331ba49 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/charmcraft.yaml @@ -0,0 +1,31 @@ +# common charmcraft.yaml file symlinked by these charms +# k8s charms can define containers + resources in metadata.yaml + +name: temporal-worker-consumer-requirer +type: charm +summary: A small charm for use in integration tests. +description: A small charm for use in integration tests. + +base: ubuntu@24.04 +platforms: + amd64: + +parts: + charm: + source: . + plugin: uv + build-snaps: [astral-uv] + +containers: + workload: + resource: workload + +resources: + workload: + type: oci-image + description: OCI image for the 'workload' container. + upstream-source: ghcr.io/canonical/ubuntu-noble:latest + +requires: + temporal-worker-consumer: + interface: temporal-worker-consumer diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/README.md b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/README.md new file mode 120000 index 000000000..8addd38ae --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/README.md @@ -0,0 +1 @@ +../../../../../../README.md \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/pyproject.toml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/pyproject.toml new file mode 120000 index 000000000..d2c52e649 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/pyproject.toml @@ -0,0 +1 @@ +../../../../../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/src b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/src new file mode 120000 index 000000000..63fbb1dd4 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/library/src @@ -0,0 +1 @@ +../../../../../../src \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/pyproject.toml b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/pyproject.toml new file mode 120000 index 000000000..00c904eb8 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/pyproject.toml @@ -0,0 +1 @@ +../../pyproject.toml \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/src/charm.py b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/src/charm.py new file mode 100644 index 000000000..b0770e419 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/src/charm.py @@ -0,0 +1,53 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""K8s charm for testing.""" + +import logging + +import common +import ops + +from charmlibs.interfaces import temporal_worker_consumer + +logger = logging.getLogger(__name__) + +CONTAINER = 'workload' + + +class Charm(common.Charm): + """Charm the application.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on[CONTAINER].pebble_ready, self._configure) + self.worker_consumer = temporal_worker_consumer.TemporalWorkerConsumerRequirer(self) + framework.observe( + self.worker_consumer.on.temporal_worker_consumer_available, self._configure + ) + + def _configure(self, event: ops.EventBase): + """Handle pebble-ready event.""" + if self.worker_consumer.namespace is None or self.worker_consumer.queue is None: + self.unit.status = ops.ActiveStatus( + 'Waiting for temporal-worker-consumer relation data' + ) + return + self.unit.status = ops.ActiveStatus( + f'Namespace: {self.worker_consumer.namespace}, Queue: {self.worker_consumer.queue}' + ) + + +if __name__ == '__main__': # pragma: nocover + ops.main(Charm) diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/src/common.py b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/src/common.py new file mode 120000 index 000000000..00a8a48b4 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/k8s/requirer/src/common.py @@ -0,0 +1 @@ +../../../common.py \ No newline at end of file diff --git a/interfaces/temporal-worker-consumer/tests/integration/charms/pyproject.toml b/interfaces/temporal-worker-consumer/tests/integration/charms/pyproject.toml new file mode 100644 index 000000000..e4a3bd413 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/charms/pyproject.toml @@ -0,0 +1,14 @@ +# common pyproject.toml file symlinked by these charms + +[project] +name = "integration-test-charm" +version = "0.0.0.dev0" +description = "Charm for integration tests." +requires-python = ">=3.10" +dependencies = [ + "ops==2.21.1", + "charmlibs-interfaces-temporal-worker-consumer", +] + +[tool.uv.sources] +"charmlibs-interfaces-temporal-worker-consumer" = { path = "library", editable = true } diff --git a/interfaces/temporal-worker-consumer/tests/integration/conftest.py b/interfaces/temporal-worker-consumer/tests/integration/conftest.py new file mode 100644 index 000000000..cb3512118 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/conftest.py @@ -0,0 +1,80 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixtures for Juju integration tests.""" + +import logging +import os +import pathlib +import sys +import time +import typing +from collections.abc import Iterator + +import jubilant +import pytest + +logger = logging.getLogger(__name__) + + +def pytest_addoption(parser: pytest.OptionGroup): + parser.addoption( + '--keep-models', + action='store_true', + default=False, + help='keep temporarily-created models', + ) + + +@pytest.fixture(scope='session') +def provider() -> str: + return 'temporal-worker-consumer-provider' + + +@pytest.fixture(scope='session') +def requirer() -> str: + return 'temporal-worker-consumer-requirer' + + +@pytest.fixture(scope='module') +def juju(request: pytest.FixtureRequest, provider: str, requirer: str) -> Iterator[jubilant.Juju]: + """Pytest fixture that wraps :meth:`jubilant.with_model`. + + This adds command line parameter ``--keep-models`` (see help for details). + """ + keep_models = typing.cast('bool', request.config.getoption('--keep-models')) + with jubilant.temp_model(keep=keep_models) as juju: + juju.model_config({'logging-config': '=INFO;unit=DEBUG'}) + _deploy(juju) + juju.wait(jubilant.all_active) + yield juju + if request.session.testsfailed: + logger.info('Collecting Juju logs ...') + time.sleep(0.5) # Wait for Juju to process logs. + log = juju.debug_log(limit=1000) + print(log, end='', file=sys.stderr) + + +def _deploy(juju: jubilant.Juju) -> None: + substrate = os.environ['CHARMLIBS_SUBSTRATE'] + # tag = os.environ.get('CHARMLIBS_TAG', '') # get the tag if needed + paths = [ + pathlib.Path(__file__).parent / '.packed' / 'provider' / f'{substrate}.charm', + pathlib.Path(__file__).parent / '.packed' / 'requirer' / f'{substrate}.charm', + ] + for path in paths: + if substrate == 'k8s': + juju.deploy(path, resources={'workload': 'ubuntu:latest'}) # name set in metadata.yaml + else: + juju.deploy(path) diff --git a/interfaces/temporal-worker-consumer/tests/integration/pack.sh b/interfaces/temporal-worker-consumer/tests/integration/pack.sh new file mode 100755 index 000000000..c37d2a288 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/pack.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# This script is executed in this directory via `just pack-k8s` or `just pack-machine`. +# Extra args are passed to this script, e.g. `just pack-k8s foo` -> $1 is 'foo'. +# In CI, the `just pack-` commands are invoked: +# - If this file exists and `just integration-` would execute any tests +# - Before running integration tests +# - With no additional arguments +# +# Environment variables: +# $CHARMLIBS_SUBSTRATE will have the value 'k8s' or 'machine' (set by pack-k8s or pack-machine) +# In CI, $CHARMLIBS_TAG is set based on pyproject.toml:tool.charmlibs.integration.tags +# For local testing, set $CHARMLIBS_TAG directly or use the tag variable. For example: +# just tag=24.04 pack-k8s some extra args +set -xueo pipefail + +TMP_DIR=".tmp" # clean temporary directory where charms will be packed +PACKED_DIR=".packed" # where packed charms will be placed with name expected in conftest.py + +: copy charm files to temporary directory for packing, dereferencing symlinks +rm -rf "$TMP_DIR" +cp --recursive --dereference "charms/$CHARMLIBS_SUBSTRATE/" "$TMP_DIR" + +: pack charm +cd "$TMP_DIR" +cd provider +uv lock # required by uv charm plugin +charmcraft pack +cd .. +cd requirer +uv lock # required by uv charm plugin +charmcraft pack +cd ../.. + +: place packed charm in expected location +mkdir -p "$PACKED_DIR" +mkdir -p "$PACKED_DIR/provider/" +mkdir -p "$PACKED_DIR/requirer/" +mv "$TMP_DIR"/provider/*.charm "$PACKED_DIR/provider/$CHARMLIBS_SUBSTRATE.charm" # read by conftest.py +mv "$TMP_DIR"/requirer/*.charm "$PACKED_DIR/requirer/$CHARMLIBS_SUBSTRATE.charm" # read by conftest.py diff --git a/interfaces/temporal-worker-consumer/tests/integration/test_version.py b/interfaces/temporal-worker-consumer/tests/integration/test_version.py new file mode 100644 index 000000000..3c1fb740a --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/test_version.py @@ -0,0 +1,26 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Integration tests using real Juju and pre-packed charm(s).""" + +import jubilant + +from charmlibs.interfaces import temporal_worker_consumer + + +def test_lib_version(juju: jubilant.Juju, provider: str, requirer: str): + result = juju.run(f'{provider}/0', 'lib-version') + assert result.results['version'] == temporal_worker_consumer.__version__ + result = juju.run(f'{requirer}/0', 'lib-version') + assert result.results['version'] == temporal_worker_consumer.__version__ diff --git a/interfaces/temporal-worker-consumer/tests/integration/test_worker_consumer.py b/interfaces/temporal-worker-consumer/tests/integration/test_worker_consumer.py new file mode 100644 index 000000000..8d36d09f9 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/integration/test_worker_consumer.py @@ -0,0 +1,48 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import jubilant + + +def test_deploy(juju: jubilant.Juju, provider: str, requirer: str): + """The deployment takes place in the module scoped `juju` fixture.""" + assert provider in juju.status().apps + assert requirer in juju.status().apps + + +def test_integrate(juju: jubilant.Juju, provider: str, requirer: str): + """Test that the relation is established.""" + namespace = 'test-ns' + queue = 'test-q' + juju.config(provider, {'namespace': namespace, 'queue': queue}) + juju.integrate(f'{provider}:temporal-worker-consumer', f'{requirer}:temporal-worker-consumer') + juju.wait(jubilant.all_active) + status = juju.status() + assert ( + status.apps[requirer].units[f'{requirer}/0'].workload_status.message + == f'Namespace: {namespace}, Queue: {queue}' + ) + + +def test_config_changed(juju: jubilant.Juju, provider: str, requirer: str): + """Test that changing the provider config updates the requirer status.""" + new_namespace = 'new-ns' + new_queue = 'new-q' + juju.config(provider, {'namespace': new_namespace, 'queue': new_queue}) + juju.wait(jubilant.all_active) + status = juju.status() + assert ( + status.apps[requirer].units[f'{requirer}/0'].workload_status.message + == f'Namespace: {new_namespace}, Queue: {new_queue}' + ) diff --git a/interfaces/temporal-worker-consumer/tests/unit/conftest.py b/interfaces/temporal-worker-consumer/tests/unit/conftest.py new file mode 100644 index 000000000..ee259deed --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/conftest.py @@ -0,0 +1,15 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixtures for unit tests, typically mocking out parts of the external system.""" diff --git a/interfaces/temporal-worker-consumer/tests/unit/dummy_provider/charmcraft.yaml b/interfaces/temporal-worker-consumer/tests/unit/dummy_provider/charmcraft.yaml new file mode 100644 index 000000000..2641ad87d --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/dummy_provider/charmcraft.yaml @@ -0,0 +1,35 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: temporal-worker-consumer-interface-provider + +description: | + Dummy temporal-worker-consumer-interface provider. +summary: | + Dummy temporal-worker-consumer-interface provider. + +provides: + temporal-worker-consumer: + interface: temporal-worker-consumer +config: + options: + namespace: + type: string + default: default-namespace + description: The Temporal namespace to use. + queue: + type: string + default: default-queue + description: The Temporal task queue to use. diff --git a/interfaces/temporal-worker-consumer/tests/unit/dummy_provider/src/charm.py b/interfaces/temporal-worker-consumer/tests/unit/dummy_provider/src/charm.py new file mode 100644 index 000000000..6fd4d19e4 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/dummy_provider/src/charm.py @@ -0,0 +1,30 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +from ops.charm import CharmBase +from ops.main import main + +from charmlibs.interfaces.temporal_worker_consumer import TemporalWorkerConsumerProvider + + +class DummyWorkerConsumerProviderCharm(CharmBase): + def __init__(self, *args: Any): + super().__init__(*args) + self.worker_consumer = TemporalWorkerConsumerProvider(self) + + +if __name__ == '__main__': + main(DummyWorkerConsumerProviderCharm) diff --git a/interfaces/temporal-worker-consumer/tests/unit/dummy_requirer/charmcraft.yaml b/interfaces/temporal-worker-consumer/tests/unit/dummy_requirer/charmcraft.yaml new file mode 100644 index 000000000..9f1547ab1 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/dummy_requirer/charmcraft.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: temporal-worker-consumer-interface-requirer + +description: | + Dummy temporal-worker-consumer-interface requirer. +summary: | + Dummy temporal-worker-consumer-interface requirer. + +requires: + temporal-worker-consumer: + interface: temporal-worker-consumer diff --git a/interfaces/temporal-worker-consumer/tests/unit/dummy_requirer/src/charm.py b/interfaces/temporal-worker-consumer/tests/unit/dummy_requirer/src/charm.py new file mode 100644 index 000000000..39c1b09e6 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/dummy_requirer/src/charm.py @@ -0,0 +1,40 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +from ops.charm import CharmBase +from ops.main import main +from scenario import ActiveStatus + +from charmlibs.interfaces.temporal_worker_consumer import TemporalWorkerConsumerRequirer + + +class DummyWorkerConsumerRequirerCharm(CharmBase): + def __init__(self, *args: Any): + super().__init__(*args) + self.worker_consumer = TemporalWorkerConsumerRequirer(self) + self.framework.observe( + self.worker_consumer.on.temporal_worker_consumer_available, + self._on_worker_consumer_available, + ) + + def _on_worker_consumer_available(self, event: Any) -> None: + namespace = self.worker_consumer.namespace + queue = self.worker_consumer.queue + self.unit.status = ActiveStatus(f'Namespace: {namespace}, Queue: {queue}') + + +if __name__ == '__main__': + main(DummyWorkerConsumerRequirerCharm) diff --git a/interfaces/temporal-worker-consumer/tests/unit/test_provides.py b/interfaces/temporal-worker-consumer/tests/unit/test_provides.py new file mode 100644 index 000000000..cdaa1b922 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/test_provides.py @@ -0,0 +1,81 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import Any + +import pytest +import yaml +from ops import testing + +from dummy_provider.src.charm import DummyWorkerConsumerProviderCharm + +METADATA: dict[str, Any] = yaml.safe_load( + (Path(__file__).parent / 'dummy_provider' / 'charmcraft.yaml').read_text() +) + + +class TestTemporalWorkerConsumerProvider: + @pytest.fixture(autouse=True) + def context(self): + self.ctx = testing.Context( + charm_type=DummyWorkerConsumerProviderCharm, meta=METADATA, config=METADATA['config'] + ) + + def test_provides(self): + relation = testing.Relation( + endpoint='temporal-worker-consumer', + interface='temporal-worker-consumer', + remote_app_name='temporal-worker-consumer-interface-requirer', + ) + state_in = testing.State( + relations={relation}, + config={'namespace': 'test-namespace', 'queue': 'test-queue'}, + leader=True, + ) + state_out = self.ctx.run(self.ctx.on.relation_changed(relation), state_in) + for r in state_out.relations: + if r.id == relation.id: + assert r.local_app_data == { + 'namespace': 'test-namespace', + 'queue': 'test-queue', + } + + def test_provides_config_changed(self): + relation = testing.Relation( + endpoint='temporal-worker-consumer', + interface='temporal-worker-consumer', + remote_app_name='temporal-worker-consumer-interface-requirer', + ) + state_in = testing.State( + relations={relation}, + config={'namespace': 'initial-namespace', 'queue': 'initial-queue'}, + leader=True, + ) + # Initial relation changed to set data + state_intermediate = self.ctx.run(self.ctx.on.relation_changed(relation), state_in) + # Now change config + state_updated = testing.State( + relations=state_intermediate.relations, + config={'namespace': 'updated-namespace', 'queue': 'updated-queue'}, + leader=True, + ) + + state_out = self.ctx.run(self.ctx.on.config_changed(), state_updated) + for r in state_out.relations: + if r.id == relation.id: + assert r.local_app_data == { + 'namespace': 'updated-namespace', + 'queue': 'updated-queue', + } diff --git a/interfaces/temporal-worker-consumer/tests/unit/test_requires.py b/interfaces/temporal-worker-consumer/tests/unit/test_requires.py new file mode 100644 index 000000000..b91a28fd8 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/test_requires.py @@ -0,0 +1,51 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import Any + +import pytest +import yaml +from ops import testing + +from dummy_requirer.src.charm import DummyWorkerConsumerRequirerCharm + +METADATA: dict[str, Any] = yaml.safe_load( + (Path(__file__).parent / 'dummy_requirer' / 'charmcraft.yaml').read_text() +) + + +class TestTemporalWorkerConsumerRequirer: + @pytest.fixture(autouse=True) + def context(self): + self.ctx = testing.Context( + charm_type=DummyWorkerConsumerRequirerCharm, + meta=METADATA, + ) + + def test_require(self): + relation = testing.Relation( + endpoint='temporal-worker-consumer', + interface='temporal-worker-consumer', + remote_app_name='temporal-worker-consumer-interface-provider', + remote_app_data={ + 'namespace': 'test-namespace', + 'queue': 'test-queue', + }, + ) + state_in = testing.State(relations={relation}) + state_out = self.ctx.run(self.ctx.on.relation_changed(relation), state_in) + assert state_out.unit_status == testing.ActiveStatus( + 'Namespace: test-namespace, Queue: test-queue' + ) diff --git a/interfaces/temporal-worker-consumer/tests/unit/test_version.py b/interfaces/temporal-worker-consumer/tests/unit/test_version.py new file mode 100644 index 000000000..5202138a6 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/test_version.py @@ -0,0 +1,21 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for library code, not involving charm code.""" + +from charmlibs.interfaces import temporal_worker_consumer + + +def test_version(): + assert isinstance(temporal_worker_consumer.__version__, str) diff --git a/interfaces/temporal-worker-consumer/tests/unit/test_version_in_charm.py b/interfaces/temporal-worker-consumer/tests/unit/test_version_in_charm.py new file mode 100644 index 000000000..fbcbf0692 --- /dev/null +++ b/interfaces/temporal-worker-consumer/tests/unit/test_version_in_charm.py @@ -0,0 +1,38 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Light weight state-transition tests of the library in a charming context.""" + +import ops +import ops.testing + +from charmlibs.interfaces import temporal_worker_consumer + + +class Charm(ops.CharmBase): + package_version: str + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + framework.observe(self.on.start, self._on_start) + + def _on_start(self, event: ops.StartEvent): + self.package_version = temporal_worker_consumer.__version__ + + +def test_version(): + ctx = ops.testing.Context(Charm, meta={'name': 'charm'}) + with ctx(ctx.on.start(), ops.testing.State()) as manager: + manager.run() + assert isinstance(manager.charm.package_version, str) diff --git a/interfaces/temporal-worker-consumer/uv.lock b/interfaces/temporal-worker-consumer/uv.lock new file mode 100644 index 000000000..6f1ee5642 --- /dev/null +++ b/interfaces/temporal-worker-consumer/uv.lock @@ -0,0 +1,308 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "charmlibs-interfaces-temporal-worker-consumer" +source = { editable = "." } +dependencies = [ + { name = "jubilant" }, + { name = "ops" }, + { name = "pytest" }, +] + +[package.dev-dependencies] +integration = [ + { name = "jubilant" }, +] +unit = [ + { name = "ops", extra = ["testing"] }, +] + +[package.metadata] +requires-dist = [ + { name = "jubilant", specifier = ">=1.5.0" }, + { name = "ops", specifier = "==2.21.1" }, + { name = "pytest", specifier = ">=8.3.5" }, +] + +[package.metadata.requires-dev] +functional = [] +integration = [{ name = "jubilant" }] +lint = [] +unit = [{ name = "ops", extras = ["testing"] }] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jubilant" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/af/2c7d2a677389eb4de3bb841f399b749ca2fd4c6c1b70313e21249536e6be/jubilant-1.5.0.tar.gz", hash = "sha256:055c65a662586191939a1a3d3e2b6d08b71ecf0c7f403a1c7ba0cde6ecaf3bbd", size = 28433, upload-time = "2025-10-10T01:08:06.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/c9/05e5aa65baa7b71b17c1314b8f25744f36fd9aad60ef9fb46fd84347885a/jubilant-1.5.0-py3-none-any.whl", hash = "sha256:eec58340b9d3d478f31e18e33281638fdf0d9b84608c8935368f7a1bb1972255", size = 28275, upload-time = "2025-10-10T01:08:04.963Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + +[[package]] +name = "ops" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "opentelemetry-api" }, + { name = "pyyaml" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/c5/f0098a9b1b72680b3682043227a628a08a7b5b9592fc98ea6efa0d638017/ops-2.21.1.tar.gz", hash = "sha256:4a8190420813ba37e7a0399d656008f99c79015d7f72e138bad7cb1ac403d0b0", size = 496427, upload-time = "2025-05-01T03:03:23.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/c7/b70271ee12418144d5c596f37745c21da105470d365d834a9fce071f7bc2/ops-2.21.1-py3-none-any.whl", hash = "sha256:6745084c8e73bc485c254f95664bd85ddcf786c91b90782f2830c8333a1440b2", size = 182682, upload-time = "2025-05-01T03:03:20.946Z" }, +] + +[package.optional-dependencies] +testing = [ + { name = "ops-scenario" }, +] + +[[package]] +name = "ops-scenario" +version = "7.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ops" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/95/90312165f3e1d5876872ad64131be9c0f26b5aa09bd73a436a21b0752820/ops_scenario-7.21.1.tar.gz", hash = "sha256:534b407b34212161c3e74cb396b79ca7449932e483053e924146fdf0140876b9", size = 141631, upload-time = "2025-05-01T03:03:30.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/3b/4cc28ea9d77015ecf8fc5c3d362bb274e35ffaa2e6fbd87909a5384afa40/ops_scenario-7.21.1-py3-none-any.whl", hash = "sha256:eeb9def2c31a1db2429cf21bd9b8cbd2ac092cc1fbe5591e850e4aa192c542fa", size = 72451, upload-time = "2025-05-01T03:03:28.488Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]