diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..98af774c6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +** @canonical/data-platform-mysql diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 000000000..e00ce3d69 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,2 @@ +github_checks: + annotations: false diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 000000000..2ac9fee57 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,90 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":disableRateLimiting", + ":noUnscheduledUpdates" + ], + "schedule": ["after 1am and before 2am every weekday"], + "lockFileMaintenance": { + "enabled": true, + "schedule": ["after 1am and before 2am every weekday"] + }, + "timezone": "Etc/UTC", + "enabledManagers": ["poetry", "github-actions", "regex"], + "packageRules": [ + // Later rules override earlier rules + { + "matchManagers": ["poetry"], + // Renovate uses "dependencies" instead of "main" for top-level dependency group + "matchDepTypes": ["dependencies"], + "rangeStrategy": "bump", + "commitMessagePrefix": "[charm]", + "groupName": "charm dependencies" + }, + { + "matchManagers": ["poetry"], + "matchDepTypes": ["charm-libs"], + "rangeStrategy": "in-range-only", + "commitMessagePrefix": "[charm lib]", + "groupName": "charm lib dependencies" + }, + { + "matchManagers": ["poetry"], + "matchDepTypes": ["format", "lint", "unit", "integration"], + "rangeStrategy": "bump", + "commitMessagePrefix": "[python ci]", + "groupName": "Python CI dependencies" + }, + // MySQL 8.X does not follow semantic versioning (e.g. 8.0.1 -> 8.0.2 can include a breaking change) + // Therefore, use a separate Renovate group so that it has a separate PR + { + "matchManagers": ["poetry"], + "matchPackageNames": ["mysql-connector-python"], + "groupName": "MySQL Connector/Python" + }, + // Group data-platform-workflows Python package & workflow updates into the same PR + { + "matchManagers": ["poetry"], + "matchPackageNames": ["canonical/data-platform-workflows"], + // Workaround for https://github.com/renovatebot/renovate/discussions/23628 + "versioning": "semver", + "groupName": "data-platform-workflows", + // Workaround: data-platform-workflows Python packages use git tags instead of pyproject.toml + // for versioning. Therefore, Renovate will always think an update is a major version update. + "separateMajorMinor": false + }, + { + "matchManagers": ["github-actions"], + "matchPackageNames": ["canonical/data-platform-workflows"], + "groupName": "data-platform-workflows", + // Workaround: data-platform-workflows Python packages use git tags instead of pyproject.toml + // for versioning. Therefore, Renovate will always think an update is a major version update. + // Since we want packages to be updated alongside workflows (actions), we must disable + // separate major PRs for workflows as well. + "separateMajorMinor": false + }, + { + "matchManagers": ["regex"], + "matchPackageNames": ["juju/juju"], + "groupName": "Juju agents" + }, + // Disable major version updates for Juju agent (they should be handled manually) + { + "matchManagers": ["regex"], + "matchPackageNames": ["juju/juju"], + "matchUpdateTypes": ["major"], + "enabled": false + }, + ], + "regexManagers": [ + { + "fileMatch": ["^\\.github/workflows/[^/]+\\.ya?ml$"], + "matchStrings": ["\"--agent-version[= ](?.*?)\" +# renovate: pin-major"], + "depNameTemplate": "juju/juju", + "extractVersionTemplate": "^juju-(?.*)$", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver" + } + ] +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a9c1da3d5..17e750072 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ on: jobs: lint: name: Lint - uses: canonical/data-platform-workflows/.github/workflows/lint.yaml@v2 + uses: canonical/data-platform-workflows/.github/workflows/lint.yaml@v4.1.0 unit-test: name: Unit tests @@ -25,9 +25,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Install tox - # TODO: Consider replacing with custom image on self-hosted runner OR pinning version - run: python3 -m pip install tox + - name: Install tox & poetry + run: | + pipx install tox + pipx install poetry - name: Run tests run: tox run -e unit - name: Upload Coverage to Codecov @@ -52,7 +53,9 @@ jobs: build: name: Build charms - uses: canonical/data-platform-workflows/.github/workflows/build_charms_with_cache.yaml@v2 + uses: canonical/data-platform-workflows/.github/workflows/build_charms_with_cache.yaml@v4.1.0 + with: + charmcraft-snap-revision: 1349 # version 2.3.0 collect-integration-tests: name: Collect integration test groups @@ -63,9 +66,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Install tox - # TODO: Consider replacing with custom image on self-hosted runner OR pinning version - run: python3 -m pip install tox + - name: Install tox & poetry + run: | + pipx install tox + pipx install poetry - name: Select test stability level id: select-test-stability run: | @@ -103,12 +107,16 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Install tox & poetry + run: | + pipx install tox + pipx install poetry - name: Setup operator environment # TODO: Replace with custom image on self-hosted runner uses: charmed-kubernetes/actions-operator@main with: provider: lxd - bootstrap-options: "--agent-version 2.9.42" # renovate: latest + bootstrap-options: "--agent-version 2.9.43" # renovate: pin-major - name: Download packed charm(s) uses: actions/download-artifact@v3 with: @@ -125,7 +133,7 @@ jobs: echo "mark_expression=not unstable" >> "$GITHUB_OUTPUT" fi - name: Run integration tests - run: tox run -e integration -- "${{ matrix.groups.path_to_test_file }}" --group ${{ matrix.groups.group_number }} -m '${{ steps.select-test-stability.outputs.mark_expression }}' --mysql-charm-series="${{ matrix.ubuntu-versions.series }}" --mysql-charm-bases-index="${{ matrix.ubuntu-versions.bases-index }}" + run: tox run -e integration -- "${{ matrix.groups.path_to_test_file }}" --group="${{ matrix.groups.group_number }}" -m '${{ steps.select-test-stability.outputs.mark_expression }}' --mysql-charm-series="${{ matrix.ubuntu-versions.series }}" --mysql-charm-bases-index="${{ matrix.ubuntu-versions.bases-index }}" env: AWS_ACCESS_KEY: ${{ matrix.groups.path_to_test_file != 'tests/integration/test_backups.py' || secrets.AWS_ACCESS_KEY }} AWS_SECRET_KEY: ${{ matrix.groups.path_to_test_file != 'tests/integration/test_backups.py' || secrets.AWS_SECRET_KEY }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 116c58f63..9629a0ed6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,14 +32,16 @@ jobs: build: name: Build charm - uses: canonical/data-platform-workflows/.github/workflows/build_charm_without_cache.yaml@v2 + uses: canonical/data-platform-workflows/.github/workflows/build_charm_without_cache.yaml@v4.1.0 + with: + charmcraft-snap-revision: 1349 # version 2.3.0 release: name: Release charm needs: - ci-tests - build - uses: canonical/data-platform-workflows/.github/workflows/release_charm.yaml@v2 + uses: canonical/data-platform-workflows/.github/workflows/release_charm.yaml@v4.1.0 with: channel: 8.0/edge artifact-name: ${{ needs.build.outputs.artifact-name }} diff --git a/.github/workflows/sync_issue_to_jira.yaml b/.github/workflows/sync_issue_to_jira.yaml index 010dbfd00..13060f034 100644 --- a/.github/workflows/sync_issue_to_jira.yaml +++ b/.github/workflows/sync_issue_to_jira.yaml @@ -9,7 +9,7 @@ on: jobs: sync: name: Sync GitHub issue to Jira - uses: canonical/data-platform-workflows/.github/workflows/sync_issue_to_jira.yaml@v2 + uses: canonical/data-platform-workflows/.github/workflows/sync_issue_to_jira.yaml@v4.1.0 with: jira-base-url: https://warthogs.atlassian.net jira-project-key: DPE diff --git a/.gitignore b/.gitignore index 75262b625..8b01ce4d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/requirements.txt +/requirements-last-build.txt *.charm *.py[cod] .coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4496e7d22..8a2425090 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,12 +20,18 @@ this operator. the `main` branch. This also avoids merge commits and creates a linear Git commit history. ## Developing +Install `tox` and `poetry` +```shell +python3 -m pip install --user pipx +python3 -m pipx ensurepath +pipx install tox +pipx install poetry +``` -You can create an environment for development with `tox`: +You can create an environment for development: ```shell -tox devenv -e integration -source venv/bin/activate +poetry install ``` ### Testing @@ -43,7 +49,7 @@ tox # runs 'lint' and 'unit' environments Build the charm in this git repository using: ```shell -charmcraft pack +tox run -e build ``` ### Deploy diff --git a/charmcraft.yaml b/charmcraft.yaml index 561b2d91b..f51e3c5bb 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -14,6 +14,13 @@ bases: channel: "22.04" parts: charm: + override-pull: | + craftctl default + if [[ ! -f requirements.txt ]] + then + echo 'ERROR: Use "tox run -e build" instead of calling "charmcraft pack" directly' >&2 + exit 1 + fi build-packages: - libffi-dev - libssl-dev diff --git a/config.yaml b/config.yaml index 66a29838a..5ab5e854f 100644 --- a/config.yaml +++ b/config.yaml @@ -5,6 +5,13 @@ options: cluster-name: description: "Optional - Name of the MySQL InnoDB cluster" type: "string" + profile: + description: | + profile representing the scope of deployment, and used to be able to enable high-level + high-level customisation of sysconfigs, resource checks/allocation, warning levels, etc. + Allowed values are: “production” and “testing”. + type: string + default: production # Config options for the legacy 'mysql relation' mysql-interface-user: description: "The database username for the legacy 'mysql' relation" diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index 86d7521a8..74db75dbe 100644 --- a/lib/charms/data_platform_libs/v0/data_interfaces.py +++ b/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Library to manage the relation for the data-platform products. +r"""Library to manage the relation for the data-platform products. This library contains the Requires and Provides classes for handling the relation between an application and multiple managed application supported by the data-team: @@ -296,17 +296,17 @@ def _on_topic_requested(self, event: TopicRequestedEvent): from abc import ABC, abstractmethod from collections import namedtuple from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Union from ops.charm import ( CharmBase, CharmEvents, RelationChangedEvent, + RelationCreatedEvent, RelationEvent, - RelationJoinedEvent, ) from ops.framework import EventSource, Object -from ops.model import Relation +from ops.model import Application, ModelError, Relation, Unit # The unique Charmhub library identifier, never change it LIBID = "6c3e6b6680d64e9c89e611d1a15f65be" @@ -316,7 +316,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 12 +LIBPATCH = 16 PYDEPS = ["ops>=2.0.0"] @@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): deleted - key that were deleted""" -def diff(event: RelationChangedEvent, bucket: str) -> Diff: +def diff(event: RelationChangedEvent, bucket: Union[Unit, Application]) -> Diff: """Retrieves the diff of the data in the relation changed databag. Args: @@ -345,9 +345,11 @@ def diff(event: RelationChangedEvent, bucket: str) -> Diff: # Retrieve the old data from the data key in the application relation databag. old_data = json.loads(event.relation.data[bucket].get("data", "{}")) # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } + new_data = ( + {key: value for key, value in event.relation.data[event.app].items() if key != "data"} + if event.app + else {} + ) # These are the keys that were added to the databag and triggered this event. added = new_data.keys() - old_data.keys() @@ -409,9 +411,11 @@ def fetch_relation_data(self) -> dict: """ data = {} for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } + data[relation.id] = ( + {key: value for key, value in relation.data[relation.app].items() if key != "data"} + if relation.app + else {} + ) return data def _update_relation_data(self, relation_id: int, data: dict) -> None: @@ -426,8 +430,8 @@ def _update_relation_data(self, relation_id: int, data: dict) -> None: that should be updated in the relation. """ if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) + if relation := self.charm.model.get_relation(self.relation_name, relation_id): + relation.data[self.local_app].update(data) @property def relations(self) -> List[Relation]: @@ -479,7 +483,7 @@ def __init__( self, charm, relation_name: str, - extra_user_roles: str = None, + extra_user_roles: Optional[str] = None, ): """Manager of base client relations.""" super().__init__(charm, relation_name) @@ -489,15 +493,15 @@ def __init__( self.local_unit = self.charm.unit self.relation_name = relation_name self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event + self.charm.on[relation_name].relation_created, self._on_relation_created_event ) self.framework.observe( self.charm.on[relation_name].relation_changed, self._on_relation_changed_event ) @abstractmethod - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the relation.""" + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the relation is created.""" raise NotImplementedError @abstractmethod @@ -517,9 +521,11 @@ def fetch_relation_data(self) -> dict: """ data = {} for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } + data[relation.id] = ( + {key: value for key, value in relation.data[relation.app].items() if key != "data"} + if relation.app + else {} + ) return data def _update_relation_data(self, relation_id: int, data: dict) -> None: @@ -563,11 +569,14 @@ def _is_relation_active(relation: Relation): try: _ = repr(relation.data) return True - except RuntimeError: + except (RuntimeError, ModelError): return False @staticmethod - def _is_resource_created_for_relation(relation: Relation): + def _is_resource_created_for_relation(relation: Relation) -> bool: + if not relation.app: + return False + return ( "username" in relation.data[relation.app] and "password" in relation.data[relation.app] ) @@ -599,10 +608,7 @@ def is_resource_created(self, relation_id: Optional[int] = None) -> bool: else: return ( all( - [ - self._is_resource_created_for_relation(relation) - for relation in self.relations - ] + self._is_resource_created_for_relation(relation) for relation in self.relations ) if self.relations else False @@ -618,6 +624,9 @@ class ExtraRoleEvent(RelationEvent): @property def extra_user_roles(self) -> Optional[str]: """Returns the extra user roles that were requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("extra-user-roles") @@ -627,21 +636,33 @@ class AuthenticationEvent(RelationEvent): @property def username(self) -> Optional[str]: """Returns the created username.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("username") @property def password(self) -> Optional[str]: """Returns the password for the created user.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("password") @property def tls(self) -> Optional[str]: """Returns whether TLS is configured.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("tls") @property def tls_ca(self) -> Optional[str]: """Returns TLS CA.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("tls-ca") @@ -654,6 +675,9 @@ class DatabaseProvidesEvent(RelationEvent): @property def database(self) -> Optional[str]: """Returns the database that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("database") @@ -676,6 +700,9 @@ class DatabaseRequiresEvent(RelationEvent): @property def database(self) -> Optional[str]: """Returns the database name.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("database") @property @@ -685,6 +712,9 @@ def endpoints(self) -> Optional[str]: In VM charms, this is the primary's address. In kubernetes charms, this is the service to the primary pod. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("endpoints") @property @@ -694,6 +724,9 @@ def read_only_endpoints(self) -> Optional[str]: In VM charms, this is the address of all the secondary instances. In kubernetes charms, this is the service to all replica pod instances. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("read-only-endpoints") @property @@ -702,6 +735,9 @@ def replset(self) -> Optional[str]: MongoDB only. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("replset") @property @@ -710,6 +746,9 @@ def uris(self) -> Optional[str]: MongoDB, Redis, OpenSearch. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("uris") @property @@ -718,6 +757,9 @@ def version(self) -> Optional[str]: Version as informed by the database daemon. """ + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("version") @@ -750,7 +792,7 @@ class DatabaseRequiresEvents(CharmEvents): class DatabaseProvides(DataProvides): """Provider-side of the database relations.""" - on = DatabaseProvidesEvents() + on = DatabaseProvidesEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -767,7 +809,9 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: # Emit a database requested event if the setup key (database name and optional # extra user roles) was added to the relation databag by the application. if "database" in diff.added: - self.on.database_requested.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "database_requested").emit( + event.relation, app=event.app, unit=event.unit + ) def set_database(self, relation_id: int, database_name: str) -> None: """Set database name. @@ -844,15 +888,15 @@ def set_version(self, relation_id: int, version: str) -> None: class DatabaseRequires(DataRequires): """Requires-side of the database relation.""" - on = DatabaseRequiresEvents() + on = DatabaseRequiresEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__( self, charm, relation_name: str, database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, + extra_user_roles: Optional[str] = None, + relations_aliases: Optional[List[str]] = None, ): """Manager of database client relations.""" super().__init__(charm, relation_name, extra_user_roles) @@ -974,7 +1018,9 @@ def is_postgresql_plugin_enabled(self, plugin: str, relation_index: int = 0) -> try: with psycopg.connect(connection_string) as connection: with connection.cursor() as cursor: - cursor.execute(f"SELECT TRUE FROM pg_extension WHERE extname='{plugin}';") + cursor.execute( + "SELECT TRUE FROM pg_extension WHERE extname=%s::text;", (plugin,) + ) return cursor.fetchone() is not None except psycopg.Error as e: logger.exception( @@ -982,8 +1028,8 @@ def is_postgresql_plugin_enabled(self, plugin: str, relation_index: int = 0) -> ) return False - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the database relation is created.""" # If relations aliases were provided, assign one to the relation. self._assign_relation_alias(event.relation.id) @@ -1010,7 +1056,9 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "username" in diff.added and "password" in diff.added: # Emit the default event (the one without an alias). logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "database_created").emit( + event.relation, app=event.app, unit=event.unit + ) # Emit the aliased event (if any). self._emit_aliased_event(event, "database_created") @@ -1024,7 +1072,9 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "endpoints" in diff.added or "endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "endpoints_changed").emit( + event.relation, app=event.app, unit=event.unit + ) # Emit the aliased event (if any). self._emit_aliased_event(event, "endpoints_changed") @@ -1038,7 +1088,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( + getattr(self.on, "read_only_endpoints_changed").emit( event.relation, app=event.app, unit=event.unit ) @@ -1055,11 +1105,17 @@ class KafkaProvidesEvent(RelationEvent): @property def topic(self) -> Optional[str]: """Returns the topic that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("topic") @property def consumer_group_prefix(self) -> Optional[str]: """Returns the consumer-group-prefix that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("consumer-group-prefix") @@ -1082,21 +1138,33 @@ class KafkaRequiresEvent(RelationEvent): @property def topic(self) -> Optional[str]: """Returns the topic.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("topic") @property def bootstrap_server(self) -> Optional[str]: """Returns a comma-separated list of broker uris.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("endpoints") @property def consumer_group_prefix(self) -> Optional[str]: """Returns the consumer-group-prefix.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("consumer-group-prefix") @property def zookeeper_uris(self) -> Optional[str]: """Returns a comma separated list of Zookeeper uris.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("zookeeper-uris") @@ -1124,7 +1192,7 @@ class KafkaRequiresEvents(CharmEvents): class KafkaProvides(DataProvides): """Provider-side of the Kafka relation.""" - on = KafkaProvidesEvents() + on = KafkaProvidesEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -1141,7 +1209,9 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: # Emit a topic requested event if the setup key (topic name and optional # extra user roles) was added to the relation databag by the application. if "topic" in diff.added: - self.on.topic_requested.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "topic_requested").emit( + event.relation, app=event.app, unit=event.unit + ) def set_topic(self, relation_id: int, topic: str) -> None: """Set topic name in the application relation databag. @@ -1183,7 +1253,7 @@ def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None: class KafkaRequires(DataRequires): """Requires-side of the Kafka relation.""" - on = KafkaRequiresEvents() + on = KafkaRequiresEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__( self, @@ -1200,8 +1270,20 @@ def __init__( self.topic = topic self.consumer_group_prefix = consumer_group_prefix or "" - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the Kafka relation.""" + @property + def topic(self): + """Topic to use in Kafka.""" + return self._topic + + @topic.setter + def topic(self, value): + # Avoid wildcards + if value == "*": + raise ValueError(f"Error on topic '{value}', cannot be a wildcard.") + self._topic = value + + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the Kafka relation is created.""" # Sets topic, extra user roles, and "consumer-group-prefix" in the relation relation_data = { f: getattr(self, f.replace("-", "_"), "") @@ -1220,7 +1302,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "username" in diff.added and "password" in diff.added: # Emit the default event (the one without an alias). logger.info("topic created at %s", datetime.now()) - self.on.topic_created.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "topic_created").emit(event.relation, app=event.app, unit=event.unit) # To avoid unnecessary application restarts do not trigger # “endpoints_changed“ event if “topic_created“ is triggered. @@ -1231,7 +1313,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "endpoints" in diff.added or "endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("endpoints changed on %s", datetime.now()) - self.on.bootstrap_server_changed.emit( + getattr(self.on, "bootstrap_server_changed").emit( event.relation, app=event.app, unit=event.unit ) # here check if this is the right design return @@ -1246,6 +1328,9 @@ class OpenSearchProvidesEvent(RelationEvent): @property def index(self) -> Optional[str]: """Returns the index that was requested.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("index") @@ -1287,7 +1372,7 @@ class OpenSearchRequiresEvents(CharmEvents): class OpenSearchProvides(DataProvides): """Provider-side of the OpenSearch relation.""" - on = OpenSearchProvidesEvents() + on = OpenSearchProvidesEvents() # pyright: ignore[reportGeneralTypeIssues] def __init__(self, charm: CharmBase, relation_name: str) -> None: super().__init__(charm, relation_name) @@ -1304,7 +1389,9 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: # Emit an index requested event if the setup key (index name and optional extra user roles) # have been added to the relation databag by the application. if "index" in diff.added: - self.on.index_requested.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "index_requested").emit( + event.relation, app=event.app, unit=event.unit + ) def set_index(self, relation_id: int, index: str) -> None: """Set the index in the application relation databag. @@ -1339,7 +1426,7 @@ def set_version(self, relation_id: int, version: str) -> None: class OpenSearchRequires(DataRequires): """Requires-side of the OpenSearch relation.""" - on = OpenSearchRequiresEvents() + on = OpenSearchRequiresEvents() # pyright: ignore[reportGeneralTypeIssues] def __init__( self, charm, relation_name: str, index: str, extra_user_roles: Optional[str] = None @@ -1349,8 +1436,8 @@ def __init__( self.charm = charm self.index = index - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the OpenSearch relation.""" + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the OpenSearch relation is created.""" # Sets both index and extra user roles in the relation if the roles are provided. # Otherwise, sets only the index. data = {"index": self.index} @@ -1371,14 +1458,16 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: updates = {"username", "password", "tls", "tls-ca"} if len(set(diff._asdict().keys()) - updates) < len(diff): logger.info("authentication updated at: %s", datetime.now()) - self.on.authentication_updated.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "authentication_updated").emit( + event.relation, app=event.app, unit=event.unit + ) # Check if the index is created # (the OpenSearch charm shares the credentials). if "username" in diff.added and "password" in diff.added: # Emit the default event (the one without an alias). logger.info("index created at: %s", datetime.now()) - self.on.index_created.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "index_created").emit(event.relation, app=event.app, unit=event.unit) # To avoid unnecessary application restarts do not trigger # “endpoints_changed“ event if “index_created“ is triggered. @@ -1389,7 +1478,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: if "endpoints" in diff.added or "endpoints" in diff.changed: # Emit the default event (the one without an alias). logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit( + getattr(self.on, "endpoints_changed").emit( event.relation, app=event.app, unit=event.unit ) # here check if this is the right design return diff --git a/lib/charms/data_platform_libs/v0/s3.py b/lib/charms/data_platform_libs/v0/s3.py index 9fb518a56..7beb113b6 100644 --- a/lib/charms/data_platform_libs/v0/s3.py +++ b/lib/charms/data_platform_libs/v0/s3.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A library for communicating with the S3 credentials providers and consumers. +r"""A library for communicating with the S3 credentials providers and consumers. This library provides the relevant interface code implementing the communication specification for fetching, retrieving, triggering, and responding to events related to @@ -113,7 +113,7 @@ def _on_credential_gone(self, event: CredentialsGoneEvent): import json import logging from collections import namedtuple -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import ops.charm import ops.framework @@ -121,15 +121,13 @@ def _on_credential_gone(self, event: CredentialsGoneEvent): from ops.charm import ( CharmBase, CharmEvents, - EventSource, - Object, - ObjectEvents, RelationBrokenEvent, RelationChangedEvent, RelationEvent, RelationJoinedEvent, ) -from ops.model import Relation +from ops.framework import EventSource, Object, ObjectEvents +from ops.model import Application, Relation, RelationDataContent, Unit # The unique Charmhub library identifier, never change it LIBID = "fca396f6254246c9bfa565b1f85ab528" @@ -139,7 +137,7 @@ def _on_credential_gone(self, event: CredentialsGoneEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 2 +LIBPATCH = 4 logger = logging.getLogger(__name__) @@ -152,7 +150,7 @@ def _on_credential_gone(self, event: CredentialsGoneEvent): deleted - key that were deleted""" -def diff(event: RelationChangedEvent, bucket: str) -> Diff: +def diff(event: RelationChangedEvent, bucket: Union[Unit, Application]) -> Diff: """Retrieves the diff of the data in the relation changed databag. Args: @@ -166,9 +164,11 @@ def diff(event: RelationChangedEvent, bucket: str) -> Diff: # Retrieve the old data from the data key in the application relation databag. old_data = json.loads(event.relation.data[bucket].get("data", "{}")) # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } + new_data = ( + {key: value for key, value in event.relation.data[event.app].items() if key != "data"} + if event.app + else {} + ) # These are the keys that were added to the databag and triggered this event. added = new_data.keys() - old_data.keys() @@ -193,7 +193,10 @@ class BucketEvent(RelationEvent): @property def bucket(self) -> Optional[str]: """Returns the bucket was requested.""" - return self.relation.data[self.relation.app].get("bucket") + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("bucket", "") class CredentialRequestedEvent(BucketEvent): @@ -209,7 +212,7 @@ class S3CredentialEvents(CharmEvents): class S3Provider(Object): """A provider handler for communicating S3 credentials to consumers.""" - on = S3CredentialEvents() + on = S3CredentialEvents() # pyright: ignore [reportGeneralTypeIssues] def __init__( self, @@ -232,7 +235,9 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: diff = self._diff(event) # emit on credential requested if bucket is provided by the requirer application if "bucket" in diff.added: - self.on.credentials_requested.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "credentials_requested").emit( + event.relation, app=event.app, unit=event.unit + ) def _load_relation_data(self, raw_relation_data: dict) -> dict: """Loads relation data from the relation data bag. @@ -242,7 +247,7 @@ def _load_relation_data(self, raw_relation_data: dict) -> dict: Returns: dict: Relation data in dict format. """ - connection_data = dict() + connection_data = {} for key in raw_relation_data: try: connection_data[key] = json.loads(raw_relation_data[key]) @@ -309,9 +314,11 @@ def fetch_relation_data(self) -> dict: """ data = {} for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } + data[relation.id] = ( + {key: value for key, value in relation.data[relation.app].items() if key != "data"} + if relation.app + else {} + ) return data def update_connection_info(self, relation_id: int, connection_data: dict) -> None: @@ -493,46 +500,73 @@ class S3Event(RelationEvent): @property def bucket(self) -> Optional[str]: """Returns the bucket name.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("bucket") @property def access_key(self) -> Optional[str]: """Returns the access key.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("access-key") @property def secret_key(self) -> Optional[str]: """Returns the secret key.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("secret-key") @property def path(self) -> Optional[str]: """Returns the path where data can be stored.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("path") @property def endpoint(self) -> Optional[str]: """Returns the endpoint address.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("endpoint") @property def region(self) -> Optional[str]: """Returns the region.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("region") @property def s3_uri_style(self) -> Optional[str]: """Returns the s3 uri style.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("s3-uri-style") @property def storage_class(self) -> Optional[str]: """Returns the storage class name.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("storage-class") @property def tls_ca_chain(self) -> Optional[List[str]]: """Returns the TLS CA chain.""" + if not self.relation.app: + return None + tls_ca_chain = self.relation.data[self.relation.app].get("tls-ca-chain") if tls_ca_chain is not None: return json.loads(tls_ca_chain) @@ -541,11 +575,17 @@ def tls_ca_chain(self) -> Optional[List[str]]: @property def s3_api_version(self) -> Optional[str]: """Returns the S3 API version.""" + if not self.relation.app: + return None + return self.relation.data[self.relation.app].get("s3-api-version") @property def attributes(self) -> Optional[List[str]]: """Returns the attributes.""" + if not self.relation.app: + return None + attributes = self.relation.data[self.relation.app].get("attributes") if attributes is not None: return json.loads(attributes) @@ -573,9 +613,11 @@ class S3CredentialRequiresEvents(ObjectEvents): class S3Requirer(Object): """Requires-side of the s3 relation.""" - on = S3CredentialRequiresEvents() + on = S3CredentialRequiresEvents() # pyright: ignore[reportGeneralTypeIssues] - def __init__(self, charm: ops.charm.CharmBase, relation_name: str, bucket_name: str = None): + def __init__( + self, charm: ops.charm.CharmBase, relation_name: str, bucket_name: Optional[str] = None + ): """Manager of the s3 client relations.""" super().__init__(charm, relation_name) @@ -658,7 +700,7 @@ def update_connection_info(self, relation_id: int, connection_data: dict) -> Non relation.data[self.local_app].update(updated_connection_data) logger.debug(f"Updated S3 credentials: {updated_connection_data}") - def _load_relation_data(self, raw_relation_data: dict) -> dict: + def _load_relation_data(self, raw_relation_data: RelationDataContent) -> Dict[str, str]: """Loads relation data from the relation data bag. Args: @@ -666,7 +708,7 @@ def _load_relation_data(self, raw_relation_data: dict) -> dict: Returns: dict: Relation data in dict format. """ - connection_data = dict() + connection_data = {} for key in raw_relation_data: try: connection_data[key] = json.loads(raw_relation_data[key]) @@ -700,22 +742,25 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: missing_options.append(configuration_option) # emit credential change event only if all mandatory fields are present if contains_required_options: - self.on.credentials_changed.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "credentials_changed").emit( + event.relation, app=event.app, unit=event.unit + ) else: logger.warning( f"Some mandatory fields: {missing_options} are not present, do not emit credential change event!" ) - def get_s3_connection_info(self) -> Dict: + def get_s3_connection_info(self) -> Dict[str, str]: """Return the s3 credentials as a dictionary.""" - relation = self.charm.model.get_relation(self.relation_name) - if not relation: - return {} - return self._load_relation_data(relation.data[relation.app]) + for relation in self.relations: + if relation and relation.app: + return self._load_relation_data(relation.data[relation.app]) + + return {} def _on_relation_broken(self, event: RelationBrokenEvent) -> None: """Notify the charm about a broken S3 credential store relation.""" - self.on.credentials_gone.emit(event.relation, app=event.app, unit=event.unit) + getattr(self.on, "credentials_gone").emit(event.relation, app=event.app, unit=event.unit) @property def relations(self) -> List[Relation]: diff --git a/lib/charms/grafana_agent/v0/cos_agent.py b/lib/charms/grafana_agent/v0/cos_agent.py index 0acaed361..9261582f5 100644 --- a/lib/charms/grafana_agent/v0/cos_agent.py +++ b/lib/charms/grafana_agent/v0/cos_agent.py @@ -22,7 +22,7 @@ Using the `COSAgentProvider` object only requires instantiating it, typically in the `__init__` method of your charm (the one which sends telemetry). -The constructor of `COSAgentProvider` has only one required and eight optional parameters: +The constructor of `COSAgentProvider` has only one required and nine optional parameters: ```python def __init__( @@ -36,6 +36,7 @@ def __init__( log_slots: Optional[List[str]] = None, dashboard_dirs: Optional[List[str]] = None, refresh_events: Optional[List] = None, + scrape_configs: Optional[Union[List[Dict], Callable]] = None, ): ``` @@ -47,7 +48,8 @@ def __init__( the `cos_agent` interface, this is where you have to specify that. - `metrics_endpoints`: In this parameter you can specify the metrics endpoints that Grafana Agent - machine Charmed Operator will scrape. + machine Charmed Operator will scrape. The configs of this list will be merged with the configs + from `scrape_configs`. - `metrics_rules_dir`: The directory in which the Charmed Operator stores its metrics alert rules files. @@ -63,6 +65,10 @@ def __init__( - `refresh_events`: List of events on which to refresh relation data. +- `scrape_configs`: List of standard scrape_configs dicts or a callable that returns the list in + case the configs need to be generated dynamically. The contents of this list will be merged + with the configs from `metrics_endpoints`. + ### Example 1 - Minimal instrumentation: @@ -91,6 +97,7 @@ def __init__(self, *args): self, relation_name="custom-cos-agent", metrics_endpoints=[ + # specify "path" and "port" to scrape from localhost {"path": "/metrics", "port": 9000}, {"path": "/metrics", "port": 9001}, {"path": "/metrics", "port": 9002}, @@ -101,6 +108,46 @@ def __init__(self, *args): log_slots=["my-app:slot"], dashboard_dirs=["./src/dashboards_1", "./src/dashboards_2"], refresh_events=["update-status", "upgrade-charm"], + scrape_configs=[ + { + "job_name": "custom_job", + "metrics_path": "/metrics", + "authorization": {"credentials": "bearer-token"}, + "static_configs": [ + { + "targets": ["localhost:9003"]}, + "labels": {"key": "value"}, + }, + ], + }, + ] + ) +``` + +### Example 3 - Dynamic scrape configs generation: + +Pass a function to the `scrape_configs` to decouple the generation of the configs +from the instantiation of the COSAgentProvider object. + +```python +from charms.grafana_agent.v0.cos_agent import COSAgentProvider +... + +class TelemetryProviderCharm(CharmBase): + def generate_scrape_configs(self): + return [ + { + "job_name": "custom", + "metrics_path": "/metrics", + "static_configs": [{"targets": ["localhost:9000"]}], + }, + ] + + def __init__(self, *args): + ... + self._grafana_agent = COSAgentProvider( + self, + scrape_configs=self.generate_scrape_configs, ) ``` @@ -166,12 +213,12 @@ def __init__(self, *args): from collections import namedtuple from itertools import chain from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Set, Union +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Optional, Set, Union import pydantic from cosl import JujuTopology from cosl.rules import AlertRules -from ops.charm import RelationChangedEvent, RelationEvent +from ops.charm import RelationChangedEvent from ops.framework import EventBase, EventSource, Object, ObjectEvents from ops.model import Relation, Unit from ops.testing import CharmType @@ -185,19 +232,19 @@ class _MetricsEndpointDict(TypedDict): port: int except ModuleNotFoundError: - _MetricsEndpointDict = dict + _MetricsEndpointDict = Dict # pyright: ignore LIBID = "dc15fa84cef84ce58155fb84f6c6213a" LIBAPI = 0 -LIBPATCH = 3 +LIBPATCH = 5 -PYDEPS = ["cosl", "pydantic"] +PYDEPS = ["cosl", "pydantic<2"] DEFAULT_RELATION_NAME = "cos-agent" DEFAULT_PEER_RELATION_NAME = "peers" -DEFAULT_METRICS_ENDPOINT = { - "path": "/metrics", - "port": 80, +DEFAULT_SCRAPE_CONFIG = { + "static_configs": [{"targets": ["localhost:80"]}], + "metrics_path": "/metrics", } logger = logging.getLogger(__name__) @@ -217,8 +264,12 @@ def _serialize(raw_json: Union[str, bytes]) -> "GrafanaDashboard": return GrafanaDashboard(encoded) def _deserialize(self) -> Dict: - raw = lzma.decompress(base64.b64decode(self.encode("utf-8"))).decode() - return json.loads(raw) + try: + raw = lzma.decompress(base64.b64decode(self.encode("utf-8"))).decode() + return json.loads(raw) + except json.decoder.JSONDecodeError as e: + logger.error("Invalid Dashboard format: %s", e) + return {} def __repr__(self): """Return string representation of self.""" @@ -247,7 +298,7 @@ class CosAgentProviderUnitData(pydantic.BaseModel): class CosAgentPeersUnitData(pydantic.BaseModel): - """Unit databag model for `cluster` cos-agent machine charm peer relation.""" + """Unit databag model for `peers` cos-agent machine charm peer relation.""" # We need the principal unit name and relation metadata to be able to render identifiers # (e.g. topology) on the leader side, after all the data moves into peer data (the grafana @@ -291,6 +342,8 @@ def __init__( log_slots: Optional[List[str]] = None, dashboard_dirs: Optional[List[str]] = None, refresh_events: Optional[List] = None, + *, + scrape_configs: Optional[Union[List[dict], Callable]] = None, ): """Create a COSAgentProvider instance. @@ -298,6 +351,8 @@ def __init__( charm: The `CharmBase` instance that is instantiating this object. relation_name: The name of the relation to communicate over. metrics_endpoints: List of endpoints in the form [{"path": path, "port": port}, ...]. + This argument is a simplified form of the `scrape_configs`. + The contents of this list will be merged with the contents of `scrape_configs`. metrics_rules_dir: Directory where the metrics rules are stored. logs_rules_dir: Directory where the logs rules are stored. recurse_rules_dirs: Whether to recurse into rule paths. @@ -305,14 +360,17 @@ def __init__( in the form ["snap-name:slot", ...]. dashboard_dirs: Directory where the dashboards are stored. refresh_events: List of events on which to refresh relation data. + scrape_configs: List of standard scrape_configs dicts or a callable + that returns the list in case the configs need to be generated dynamically. + The contents of this list will be merged with the contents of `metrics_endpoints`. """ super().__init__(charm, relation_name) - metrics_endpoints = metrics_endpoints or [DEFAULT_METRICS_ENDPOINT] dashboard_dirs = dashboard_dirs or ["./src/grafana_dashboards"] self._charm = charm self._relation_name = relation_name - self._metrics_endpoints = metrics_endpoints + self._metrics_endpoints = metrics_endpoints or [] + self._scrape_configs = scrape_configs or [] self._metrics_rules = metrics_rules_dir self._logs_rules = logs_rules_dir self._recursive = recurse_rules_dirs @@ -328,10 +386,7 @@ def __init__( def _on_refresh(self, event): """Trigger the class to update relation data.""" - if isinstance(event, RelationEvent): - relations = [event.relation] - else: - relations = self._charm.model.relations[self._relation_name] + relations = self._charm.model.relations[self._relation_name] for relation in relations: # Before a principal is related to the grafana-agent subordinate, we'd get @@ -339,23 +394,51 @@ def _on_refresh(self, event): # Add a guard to make sure it doesn't happen. if relation.data and self._charm.unit in relation.data: # Subordinate relations can communicate only over unit data. - data = CosAgentProviderUnitData( - metrics_alert_rules=self._metrics_alert_rules, - log_alert_rules=self._log_alert_rules, - dashboards=self._dashboards, - metrics_scrape_jobs=self._scrape_jobs, - log_slots=self._log_slots, - ) - relation.data[self._charm.unit][data.KEY] = data.json() + try: + data = CosAgentProviderUnitData( + metrics_alert_rules=self._metrics_alert_rules, + log_alert_rules=self._log_alert_rules, + dashboards=self._dashboards, + metrics_scrape_jobs=self._scrape_jobs, + log_slots=self._log_slots, + ) + relation.data[self._charm.unit][data.KEY] = data.json() + except ( + pydantic.ValidationError, + json.decoder.JSONDecodeError, + ) as e: + logger.error("Invalid relation data provided: %s", e) @property def _scrape_jobs(self) -> List[Dict]: - """Return a prometheus_scrape-like data structure for jobs.""" - job_name_prefix = self._charm.app.name - return [ - {"job_name": f"{job_name_prefix}_{key}", **endpoint} - for key, endpoint in enumerate(self._metrics_endpoints) - ] + """Return a prometheus_scrape-like data structure for jobs. + + https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config + """ + if callable(self._scrape_configs): + scrape_configs = self._scrape_configs() + else: + # Create a copy of the user scrape_configs, since we will mutate this object + scrape_configs = self._scrape_configs.copy() + + # Convert "metrics_endpoints" to standard scrape_configs, and add them in + for endpoint in self._metrics_endpoints: + scrape_configs.append( + { + "metrics_path": endpoint["path"], + "static_configs": [{"targets": [f"localhost:{endpoint['port']}"]}], + } + ) + + scrape_configs = scrape_configs or [DEFAULT_SCRAPE_CONFIG] + + # Augment job name to include the app name and a unique id (index) + for idx, scrape_config in enumerate(scrape_configs): + scrape_config["job_name"] = "_".join( + [self._charm.app.name, str(idx), scrape_config.get("job_name", "default")] + ) + + return scrape_configs @property def _metrics_alert_rules(self) -> Dict: @@ -387,16 +470,33 @@ class COSAgentDataChanged(EventBase): """Event emitted by `COSAgentRequirer` when relation data changes.""" +class COSAgentValidationError(EventBase): + """Event emitted by `COSAgentRequirer` when there is an error in the relation data.""" + + def __init__(self, handle, message: str = ""): + super().__init__(handle) + self.message = message + + def snapshot(self) -> Dict: + """Save COSAgentValidationError source information.""" + return {"message": self.message} + + def restore(self, snapshot): + """Restore COSAgentValidationError source information.""" + self.message = snapshot["message"] + + class COSAgentRequirerEvents(ObjectEvents): """`COSAgentRequirer` events.""" data_changed = EventSource(COSAgentDataChanged) + validation_error = EventSource(COSAgentValidationError) class COSAgentRequirer(Object): """Integration endpoint wrapper for the Requirer side of the cos_agent interface.""" - on = COSAgentRequirerEvents() + on = COSAgentRequirerEvents() # pyright: ignore def __init__( self, @@ -426,7 +526,7 @@ def __init__( ) # TODO: do we need this? self.framework.observe(events.relation_changed, self._on_relation_data_changed) for event in self._refresh_events: - self.framework.observe(event, self.trigger_refresh) + self.framework.observe(event, self.trigger_refresh) # pyright: ignore # Peer relation events # A peer relation is needed as it is the only mechanism for exchanging data across @@ -450,7 +550,7 @@ def _on_peer_relation_changed(self, _): # Peer data is used for forwarding data from principal units to the grafana agent # subordinate leader, for updating the app data of the outgoing o11y relations. if self._charm.unit.is_leader(): - self.on.data_changed.emit() + self.on.data_changed.emit() # pyright: ignore def _on_relation_data_changed(self, event: RelationChangedEvent): # Peer data is the only means of communication between subordinate units. @@ -474,7 +574,9 @@ def _on_relation_data_changed(self, event: RelationChangedEvent): if not (raw := cos_agent_relation.data[principal_unit].get(CosAgentProviderUnitData.KEY)): return - provider_data = CosAgentProviderUnitData(**json.loads(raw)) + + if not (provider_data := self._validated_provider_data(raw)): + return # Copy data from the principal relation to the peer relation, so the leader could # follow up. @@ -492,12 +594,19 @@ def _on_relation_data_changed(self, event: RelationChangedEvent): # We can't easily tell if the data that was changed is limited to only the data # that goes into peer relation (in which case, if this is not a leader unit, we wouldn't # need to emit `on.data_changed`), so we're emitting `on.data_changed` either way. - self.on.data_changed.emit() + self.on.data_changed.emit() # pyright: ignore + + def _validated_provider_data(self, raw) -> Optional[CosAgentProviderUnitData]: + try: + return CosAgentProviderUnitData(**json.loads(raw)) + except (pydantic.ValidationError, json.decoder.JSONDecodeError) as e: + self.on.validation_error.emit(message=str(e)) # pyright: ignore + return None def trigger_refresh(self, _): """Trigger a refresh of relation data.""" # FIXME: Figure out what we should do here - self.on.data_changed.emit() + self.on.data_changed.emit() # pyright: ignore @property def _principal_unit(self) -> Optional[Unit]: @@ -529,17 +638,24 @@ def _principal_unit_data(self) -> Optional[CosAgentProviderUnitData]: Relies on the fact that, for subordinate relations, the only remote unit visible to *this unit* is the principal unit that this unit is attached to. """ - if relations := self._principal_relations: - # Technically it's a list, but for subordinates there can only be one relation - principal_relation = next(iter(relations)) - if units := principal_relation.units: - # Technically it's a list, but for subordinates there can only be one - unit = next(iter(units)) - raw = principal_relation.data[unit].get(CosAgentProviderUnitData.KEY) - if raw: - return CosAgentProviderUnitData(**json.loads(raw)) + if not (relations := self._principal_relations): + return None - return None + # Technically it's a list, but for subordinates there can only be one relation + principal_relation = next(iter(relations)) + + if not (units := principal_relation.units): + return None + + # Technically it's a list, but for subordinates there can only be one + unit = next(iter(units)) + if not (raw := principal_relation.data[unit].get(CosAgentProviderUnitData.KEY)): + return None + + if not (provider_data := self._validated_provider_data(raw)): + return None + + return provider_data def _gather_peer_data(self) -> List[CosAgentPeersUnitData]: """Collect data from the peers. @@ -578,7 +694,7 @@ def metrics_alerts(self) -> Dict[str, Any]: alert_rules = {} seen_apps: List[str] = [] - for data in self._gather_peer_data(): # type: CosAgentPeersUnitData + for data in self._gather_peer_data(): if rules := data.metrics_alert_rules: app_name = data.app_name if app_name in seen_apps: @@ -605,15 +721,18 @@ def metrics_jobs(self) -> List[Dict]: """Parse the relation data contents and extract the metrics jobs.""" scrape_jobs = [] if data := self._principal_unit_data: - jobs = data.metrics_scrape_jobs - if jobs: - for job in jobs: - job_config = { + for job in data.metrics_scrape_jobs: + # In #220, relation schema changed from a simplified dict to the standard + # `scrape_configs`. + # This is to ensure backwards compatibility with Providers older than v0.5. + if "path" in job and "port" in job and "job_name" in job: + job = { "job_name": job["job_name"], "metrics_path": job["path"], "static_configs": [{"targets": [f"localhost:{job['port']}"]}], } - scrape_jobs.append(job_config) + + scrape_jobs.append(job) return scrape_jobs @@ -649,7 +768,7 @@ def logs_alerts(self) -> Dict[str, Any]: alert_rules = {} seen_apps: List[str] = [] - for data in self._gather_peer_data(): # type: CosAgentPeersUnitData + for data in self._gather_peer_data(): if rules := data.log_alert_rules: # This is only used for naming the file, so be as specific as we can be app_name = data.app_name @@ -678,10 +797,10 @@ def dashboards(self) -> List[Dict[str, str]]: Dashboards are assumed not to vary across units of the same primary. """ - dashboards: List[Dict[str, str]] = [] + dashboards: List[Dict[str, Any]] = [] seen_apps: List[str] = [] - for data in self._gather_peer_data(): # type: CosAgentPeersUnitData + for data in self._gather_peer_data(): app_name = data.app_name if app_name in seen_apps: continue # dedup! diff --git a/lib/charms/mysql/v0/backups.py b/lib/charms/mysql/v0/backups.py index c8c079b2e..72c8b015b 100644 --- a/lib/charms/mysql/v0/backups.py +++ b/lib/charms/mysql/v0/backups.py @@ -48,6 +48,7 @@ def is_unit_blocked(self) -> bool: import datetime import logging import pathlib +import typing from typing import Dict, List, Optional, Tuple from charms.data_platform_libs.v0.s3 import S3Requirer @@ -76,7 +77,7 @@ def is_unit_blocked(self) -> bool: list_backups_in_s3_path, upload_content_to_s3, ) -from ops.charm import ActionEvent, CharmBase +from ops.charm import ActionEvent from ops.framework import Object from ops.jujuversion import JujuVersion from ops.model import ActiveStatus, BlockedStatus @@ -95,13 +96,17 @@ def is_unit_blocked(self) -> bool: # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 6 +LIBPATCH = 7 + + +if typing.TYPE_CHECKING: + from charm import MySQLOperatorCharm class MySQLBackups(Object): """Encapsulation of backups for MySQL.""" - def __init__(self, charm: CharmBase, s3_integrator: S3Requirer) -> None: + def __init__(self, charm: "MySQLOperatorCharm", s3_integrator: S3Requirer) -> None: super().__init__(charm, MYSQL_BACKUPS) self.charm = charm @@ -251,7 +256,7 @@ def _on_create_backup(self, event: ActionEvent) -> None: can_unit_perform_backup, validation_message = self._can_unit_perform_backup() if not can_unit_perform_backup: logger.error(f"Backup failed: {validation_message}") - event.fail(validation_message) + event.fail(validation_message or "") return # Test uploading metadata to S3 to test credentials before backup @@ -272,14 +277,14 @@ def _on_create_backup(self, event: ActionEvent) -> None: success, error_message = self._pre_backup() if not success: logger.error(f"Backup failed: {error_message}") - event.fail(error_message) + event.fail(error_message or "") return # Perform the backup success, error_message = self._backup(backup_path, s3_parameters) if not success: logger.error(f"Backup failed: {error_message}") - event.fail(error_message) + event.fail(error_message or "") success, error_message = self._post_backup() if not success: @@ -297,7 +302,7 @@ def _on_create_backup(self, event: ActionEvent) -> None: self.charm.unit.status = BlockedStatus( "Failed to create backup; instance in bad state" ) - event.fail(error_message) + event.fail(error_message or "") return logger.info(f"Backup succeeded: with backup-id {datetime_backup_requested}") diff --git a/lib/charms/mysql/v0/mysql.py b/lib/charms/mysql/v0/mysql.py index 90dfcafb8..ebbd23640 100644 --- a/lib/charms/mysql/v0/mysql.py +++ b/lib/charms/mysql/v0/mysql.py @@ -73,6 +73,8 @@ def wait_until_mysql_connection(self) -> None: from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, List, Optional, Tuple +import ops +from ops.charm import ActionEvent, CharmBase from tenacity import ( retry, retry_if_exception_type, @@ -81,6 +83,23 @@ def wait_until_mysql_connection(self) -> None: wait_random, ) +from constants import ( + BACKUPS_PASSWORD_KEY, + BACKUPS_USERNAME, + CLUSTER_ADMIN_PASSWORD_KEY, + CLUSTER_ADMIN_USERNAME, + MONITORING_PASSWORD_KEY, + MONITORING_USERNAME, + PASSWORD_LENGTH, + PEER, + ROOT_PASSWORD_KEY, + ROOT_USERNAME, + SECRET_ID_KEY, + SERVER_CONFIG_PASSWORD_KEY, + SERVER_CONFIG_USERNAME, +) +from utils import generate_random_password + logger = logging.getLogger(__name__) # The unique Charmhub library identifier, never change it @@ -91,11 +110,15 @@ def wait_until_mysql_connection(self) -> None: # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 35 +LIBPATCH = 38 UNIT_TEARDOWN_LOCKNAME = "unit-teardown" UNIT_ADD_LOCKNAME = "unit-add" +BYTES_1GiB = 1073741824 # 1 gibibyte +BYTES_1GB = 1000000000 # 1 gigabyte +BYTES_1MiB = 1048576 # 1 mebibyte + class Error(Exception): """Base class for exceptions in this module.""" @@ -303,6 +326,10 @@ class MySQLRescanClusterError(Error): """Exception raised when there is an issue rescanning the cluster.""" +class MySQLSecretError(Error): + """Exception raised when there is an issue setting/getting a secret.""" + + @dataclasses.dataclass class RouterUser: """MySQL Router user.""" @@ -311,6 +338,276 @@ class RouterUser: router_id: str +class MySQLCharmBase(CharmBase): + """Base class to encapsulate charm related functionality. + + Meant as a means to share common charm related code between the MySQL VM and + K8s charms. + """ + + def __init__(self, *args): + super().__init__(*args) + + self.app_secrets, self.unit_secrets = None, None + + self.framework.observe(self.on.get_cluster_status_action, self._get_cluster_status) + self.framework.observe(self.on.get_password_action, self._on_get_password) + self.framework.observe(self.on.set_password_action, self._on_set_password) + + def _on_get_password(self, event: ActionEvent) -> None: + """Action used to retrieve the system user's password.""" + username = event.params.get("username") or ROOT_USERNAME + + valid_usernames = { + ROOT_USERNAME: ROOT_PASSWORD_KEY, + SERVER_CONFIG_USERNAME: SERVER_CONFIG_PASSWORD_KEY, + CLUSTER_ADMIN_USERNAME: CLUSTER_ADMIN_PASSWORD_KEY, + MONITORING_USERNAME: MONITORING_PASSWORD_KEY, + BACKUPS_USERNAME: BACKUPS_PASSWORD_KEY, + } + + secret_key = valid_usernames.get(username) + if not secret_key: + event.fail( + f"The action can be run only for users used by the charm: {', '.join(valid_usernames.keys())} not {username}" + ) + return + + event.set_results({"username": username, "password": self.get_secret("app", secret_key)}) + + def _on_set_password(self, event: ActionEvent) -> None: + """Action used to update/rotate the system user's password.""" + if not self.unit.is_leader(): + event.fail("set-password action can only be run on the leader unit.") + return + + username = event.params.get("username") or ROOT_USERNAME + + valid_usernames = { + ROOT_USERNAME: ROOT_PASSWORD_KEY, + SERVER_CONFIG_USERNAME: SERVER_CONFIG_PASSWORD_KEY, + CLUSTER_ADMIN_USERNAME: CLUSTER_ADMIN_PASSWORD_KEY, + MONITORING_USERNAME: MONITORING_PASSWORD_KEY, + BACKUPS_USERNAME: BACKUPS_PASSWORD_KEY, + } + + secret_key = valid_usernames.get(username) + if not secret_key: + event.fail( + f"The action can be run only for users used by the charm: {', '.join(valid_usernames.keys())} not {username}" + ) + return + + new_password = event.params.get("password") or generate_random_password(PASSWORD_LENGTH) + + self._mysql.update_user_password(username, new_password) + + self.set_secret("app", secret_key, new_password) + + def _get_cluster_status(self, event: ActionEvent) -> None: + """Action used to retrieve the cluster status.""" + if status := self._mysql.get_cluster_status(): + event.set_results( + { + "success": True, + "status": status, + } + ) + else: + event.set_results( + { + "success": False, + "message": "Failed to read cluster status. See logs for more information.", + } + ) + + @property + def peers(self) -> ops.model.Relation: + """Retrieve the peer relation.""" + return self.model.get_relation(PEER) + + @property + def cluster_initialized(self): + """Returns True if the cluster is initialized.""" + return int(self.app_peer_data.get("units-added-to-cluster", "0")) >= 1 + + @property + def unit_initialized(self): + """Return True if the unit is initialized.""" + return self.unit_peer_data.get("unit-initialized") == "True" + + @property + def app_peer_data(self) -> Dict: + """Application peer relation data object.""" + if self.peers is None: + return {} + + return self.peers.data[self.app] + + @property + def unit_peer_data(self) -> Dict: + """Unit peer relation data object.""" + if self.peers is None: + return {} + + return self.peers.data[self.unit] + + @property + def _is_peer_data_set(self): + return bool( + self.app_peer_data.get("cluster-name") + and self.get_secret("app", ROOT_PASSWORD_KEY) + and self.get_secret("app", SERVER_CONFIG_PASSWORD_KEY) + and self.get_secret("app", CLUSTER_ADMIN_PASSWORD_KEY) + and self.get_secret("app", MONITORING_PASSWORD_KEY) + and self.get_secret("app", BACKUPS_PASSWORD_KEY) + ) + + def _get_secret_from_juju(self, scope: str, key: str) -> Optional[str]: + """Retrieve and return the secret from the juju secret storage.""" + if scope == "unit": + secret_id = self.unit_peer_data.get(SECRET_ID_KEY) + + if not self.unit_secrets and not secret_id: + logger.debug("Getting a secret when no secrets added in juju") + return None + + if not self.unit_secrets: + secret = self.model.get_secret(id=secret_id) + content = secret.get_content() + self.unit_secrets = content + + logger.debug(f"Retrieved secret {key} for unit") + return self.unit_secrets.get(key) + + secret_id = self.app_peer_data.get(SECRET_ID_KEY) + + if not self.app_secrets and not secret_id: + logger.debug("Getting a secret when no secrets added in juju") + return None + + if not self.app_secrets: + secret = self.model.get_secret(id=secret_id) + content = secret.get_content() + self.app_secrets = content + + logger.debug(f"Retrieved secret {key} for app") + return self.app_secrets.get(key) + + def _get_secret_from_databag(self, scope: str, key: Optional[str]) -> Optional[str]: + """Retrieve and return the secret from the peer relation databag.""" + if scope == "unit": + return self.unit_peer_data.get(key) + + return self.app_peer_data.get(key) + + def get_secret( + self, scope: str, key: str, fallback_key: Optional[str] = None + ) -> Optional[str]: + """Get secret from the secret storage. + + Retrieve secret from juju secrets backend if secret exists there. + Else retrieve from peer databag (with key or fallback_key). This is to + account for cases where secrets are stored in peer databag but the charm + is then refreshed to a newer revision. + """ + if scope not in ["unit", "app"]: + raise MySQLSecretError(f"Invalid secret scope: {scope}") + + if ops.jujuversion.JujuVersion.from_environ().has_secrets: + secret = self._get_secret_from_juju(scope, key) + if secret: + return secret + + return self._get_secret_from_databag(scope, key) or self._get_secret_from_databag( + scope, fallback_key + ) + + def _set_secret_in_databag(self, scope: str, key: str, value: str) -> None: + """Set secret in the peer relation databag.""" + if not value: + if scope == "unit": + del self.unit_peer_data[key] + else: + del self.app_peer_data[key] + return + + if scope == "unit": + self.unit_peer_data[key] = value + return + + self.app_peer_data[key] = value + + def _set_secret_in_juju(self, scope: str, key: str, value: str) -> None: + """Set the secret in the juju secret storage.""" + if scope == "unit": + secret_id = self.unit_peer_data.get(SECRET_ID_KEY) + else: + secret_id = self.app_peer_data.get(SECRET_ID_KEY) + + if secret_id: + secret = self.model.get_secret(id=secret_id) + + if scope == "unit": + content = self.unit_secrets or secret.get_content() + else: + content = self.app_secrets or secret.get_content() + + if not value: + del content[key] + else: + content[key] = value + + secret.set_content(content) + logger.debug(f"Updated {scope} secret {secret_id} for {key}") + elif not value: + return + else: + content = { + key: value, + } + + if scope == "unit": + secret = self.unit.add_secret(content) + self.unit_peer_data[SECRET_ID_KEY] = secret.id + else: + secret = self.app.add_secret(content) + self.app_peer_data[SECRET_ID_KEY] = secret.id + logger.debug(f"Added {scope} secret {secret_id} for {key}") + + if scope == "unit": + self.unit_secrets = content + else: + self.app_secrets = content + + def set_secret( + self, scope: str, key: str, value: Optional[str], fallback_key: Optional[str] = None + ) -> None: + """Set a secret in the secret storage.""" + if scope not in ["unit", "app"]: + raise MySQLSecretError(f"Invalid secret scope: {scope}") + + if scope == "app" and not self.unit.is_leader(): + raise MySQLSecretError("Can only set app secrets on the leader unit") + + if ops.jujuversion.JujuVersion.from_environ().has_secrets: + self._set_secret_in_juju(scope, key, value) + + # for refresh from juju <= 3.1.4 to >= 3.1.5, we need to clear out + # secrets from the databag as well + if self._get_secret_from_databag(scope, key): + self._set_secret_in_databag(scope, key, None) + + if fallback_key and self._get_secret_from_databag(scope, fallback_key): + self._set_secret_in_databag(scope, key, None) + + return + + self._set_secret_in_databag(scope, key, value) + if fallback_key: + self._set_secret_in_databag(scope, fallback_key, None) + + class MySQLBase(ABC): """Abstract class to encapsulate all operations related to the MySQL instance and cluster. @@ -515,7 +812,7 @@ def create_application_database_and_scoped_user( password: str, hostname: str, *, - unit_name: str = None, + unit_name: Optional[str] = None, create_database: bool = True, ) -> None: """Create an application database and a user scoped to the created database. @@ -1248,7 +1545,9 @@ def _get_cluster_member_addresses(self, exclude_unit_labels: List = []) -> Tuple return (member_addresses, "" in output) - def get_cluster_primary_address(self, connect_instance_address: str = None) -> Optional[str]: + def get_cluster_primary_address( + self, connect_instance_address: Optional[str] = None + ) -> Optional[str]: """Get the cluster primary's address. Keyword args: @@ -1393,8 +1692,8 @@ def get_member_state(self) -> Tuple[str, str]: A tuple(str) with the MEMBER_STATE and MEMBER_ROLE within the cluster. """ member_state_query = ( - "SELECT MEMBER_STATE, MEMBER_ROLE FROM" - " performance_schema.replication_group_members WHERE MEMBER_ID = @@server_uuid" + "SELECT MEMBER_STATE, MEMBER_ROLE, MEMBER_ID, @@server_uuid" + " FROM performance_schema.replication_group_members" ) try: @@ -1410,13 +1709,27 @@ def get_member_state(self) -> Tuple[str, str]: ) raise MySQLGetMemberStateError(e.message) - lines = output.lower().split("\n") + # output is like: + # 'MEMBER_STATE\tMEMBER_ROLE\tMEMBER_ID\t@@server_uuid\nONLINE\tPRIMARY\t\t\n' + lines = output.strip().lower().split("\n") if len(lines) < 2: raise MySQLGetMemberStateError("No member state retrieved") - results = lines[1].split() - # no member role defined when member state is 'offline' - return results[0], results[1] if len(results) == 2 else "unknown" + if len(lines) == 2: + # Instance just know it own state + # sometimes member_id is not populated + results = lines[1].split("\t") + return results[0], results[1] or "unknown" + + for line in lines[1:]: + # results will be like: + # ['online', 'primary', 'a6c00302-1c07-11ee-bca1-...', 'a6c00302-1c07-11ee-bca1-...'] + results = line.split("\t") + if results[2] == results[3]: + # filter server uuid + return results[0], results[1] or "unknown" + + raise MySQLGetMemberStateError("No member state retrieved") def reboot_from_complete_outage(self) -> None: """Wrapper for reboot_cluster_from_complete_outage command.""" @@ -1502,32 +1815,36 @@ def offline_mode_and_hidden_instance_exists(self) -> bool: return matches.group(1) != "0" - def get_innodb_buffer_pool_parameters(self) -> Tuple[int, Optional[int]]: + def get_innodb_buffer_pool_parameters(self) -> Tuple[int, Optional[int], Optional[int]]: """Get innodb buffer pool parameters for the instance. - Returns: a tuple of (innodb_buffer_pool_size, optional(innodb_buffer_pool_chunk_size)) + Returns: + a tuple of (innodb_buffer_pool_size, optional(innodb_buffer_pool_chunk_size), + optional(group_replication_message_cache)) """ # Reference: based off xtradb-cluster-operator # https://github.com/percona/percona-xtradb-cluster-operator/blob/main/pkg/pxc/app/config/autotune.go#L31-L54 - chunk_size_min = 1048576 # 1 mebibyte - chunk_size_default = 134217728 # 128 mebibytes + chunk_size_min = BYTES_1MiB + chunk_size_default = 128 * BYTES_1MiB + group_replication_message_cache_default = BYTES_1GiB try: innodb_buffer_pool_chunk_size = None + group_replication_message_cache = None total_memory = self._get_total_memory() - pool_size = int(total_memory * 0.75) - # 1000000000 = 1 gigabyte - if total_memory - pool_size < 1000000000: + pool_size = int(total_memory * 0.75) - group_replication_message_cache_default + + if pool_size < 0 or total_memory - pool_size < BYTES_1GB: + group_replication_message_cache = 128 * BYTES_1MiB pool_size = int(total_memory * 0.5) if pool_size % chunk_size_default != 0: # round pool_size to be a multiple of chunk_size_default pool_size += chunk_size_default - (pool_size % chunk_size_default) - # 1073741824 = 1 gibibyte - if pool_size > 1073741824: + if pool_size > BYTES_1GiB: chunk_size = int(pool_size / 8) if chunk_size % chunk_size_min != 0: @@ -1538,7 +1855,7 @@ def get_innodb_buffer_pool_parameters(self) -> Tuple[int, Optional[int]]: innodb_buffer_pool_chunk_size = chunk_size - return (pool_size, innodb_buffer_pool_chunk_size) + return (pool_size, innodb_buffer_pool_chunk_size, group_replication_message_cache) except Exception: logger.exception("Failed to compute innodb buffer pool parameters") raise MySQLGetAutoTunningParametersError("Error computing buffer pool parameters") @@ -1548,7 +1865,7 @@ def get_max_connections(self) -> int: # Reference: based off xtradb-cluster-operator # https://github.com/percona/percona-xtradb-cluster-operator/blob/main/pkg/pxc/app/config/autotune.go#L61-L70 - bytes_per_connection = 12582912 # 12 Megabytes + bytes_per_connection = 12 * BYTES_1MiB total_memory = 0 try: @@ -1595,8 +1912,8 @@ def execute_backup_commands( mysqld_socket_file: str, tmp_base_directory: str, defaults_config_file: str, - user: str = None, - group: str = None, + user: Optional[str] = None, + group: Optional[str] = None, ) -> Tuple[str, str]: """Executes commands to create a backup with the given args.""" nproc_command = "nproc".split() @@ -1672,8 +1989,8 @@ def execute_backup_commands( def delete_temp_backup_directory( self, tmp_base_directory: str, - user: str = None, - group: str = None, + user: Optional[str] = None, + group: Optional[str] = None, ) -> None: """Delete the temp backup directory.""" delete_temp_dir_command = f"find {tmp_base_directory} -wholename {tmp_base_directory}/xtra_backup_* -delete".split() @@ -1778,7 +2095,7 @@ def prepare_backup_for_restore( ) -> Tuple[str, str]: """Prepare the backup in the provided dir for restore.""" try: - innodb_buffer_pool_size, _ = self.get_innodb_buffer_pool_parameters() + innodb_buffer_pool_size, _, _ = self.get_innodb_buffer_pool_parameters() except MySQLGetAutoTunningParametersError as e: raise MySQLPrepareBackupForRestoreError(e) @@ -1896,8 +2213,8 @@ def _execute_commands( self, commands: List[str], bash: bool = False, - user: str = None, - group: str = None, + user: Optional[str] = None, + group: Optional[str] = None, env: Dict = {}, ) -> Tuple[str, str]: """Execute commands on the server where MySQL is running.""" @@ -1935,6 +2252,7 @@ def tls_setup( password=self.server_config_password, ) except MySQLClientError: + logger.exception("Failed to set custom TLS configuration") raise MySQLTLSSetupError("Failed to set custom TLS configuration") def kill_unencrypted_sessions(self) -> None: @@ -2002,7 +2320,11 @@ def _run_mysqlsh_script(self, script: str, timeout: Optional[int] = None) -> str @abstractmethod def _run_mysqlcli_script( - self, script: str, user: str = "root", password: str = None, timeout: Optional[int] = None + self, + script: str, + user: str = "root", + password: Optional[str] = None, + timeout: Optional[int] = None, ) -> str: """Execute a MySQL CLI script. diff --git a/lib/charms/mysql/v0/tls.py b/lib/charms/mysql/v0/tls.py index 15af986de..b51cbe976 100644 --- a/lib/charms/mysql/v0/tls.py +++ b/lib/charms/mysql/v0/tls.py @@ -52,7 +52,7 @@ LIBAPI = 0 -LIBPATCH = 1 +LIBPATCH = 2 SCOPE = "unit" @@ -118,8 +118,8 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: self.charm.set_secret( SCOPE, "chain", "\n".join(event.chain) if event.chain is not None else None ) - self.charm.set_secret(SCOPE, "cert", event.certificate) - self.charm.set_secret(SCOPE, "ca", event.ca) + self.charm.set_secret(SCOPE, "certificate", event.certificate, fallback_key="cert") + self.charm.set_secret(SCOPE, "certificate-authority", event.ca, fallback_key="ca") self.push_tls_files_to_workload() try: @@ -144,7 +144,7 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: """Request the new certificate when old certificate is expiring.""" - if event.certificate != self.charm.get_secret(SCOPE, "cert"): + if event.certificate != self.charm.get_secret(SCOPE, "certificate", fallback_key="cert"): logger.error("An unknown certificate expiring.") return @@ -165,8 +165,8 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: def _on_tls_relation_broken(self, _) -> None: """Disable TLS when TLS relation broken.""" try: - self.charm.set_secret(SCOPE, "ca", None) - self.charm.set_secret(SCOPE, "cert", None) + self.charm.set_secret(SCOPE, "certificate-authority", None, fallback_key="ca") + self.charm.set_secret(SCOPE, "certificate", None, fallback_key="cert") self.charm.set_secret(SCOPE, "chain", None) except KeyError: # ignore key error for unit teardown @@ -235,12 +235,12 @@ def get_tls_content(self) -> Tuple[Optional[str], Optional[str], Optional[str]]: Returns: A tuple of strings with the content of server-key, ca and server-cert """ - ca = self.charm.get_secret(SCOPE, "ca") + ca = self.charm.get_secret(SCOPE, "certificate-authority", fallback_key="ca") chain = self.charm.get_secret(SCOPE, "chain") ca_file = chain or ca key = self.charm.get_secret(SCOPE, "key") - cert = self.charm.get_secret(SCOPE, "cert") + cert = self.charm.get_secret(SCOPE, "certificate", fallback_key="cert") return key, ca_file, cert def push_tls_files_to_workload(self) -> None: diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..9c826a519 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2162 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + +[[package]] +name = "bcrypt" +version = "4.0.1" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.6" +files = [ + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "black" +version = "23.7.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "boto3" +version = "1.28.15" +description = "The AWS SDK for Python" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "boto3-1.28.15-py3-none-any.whl", hash = "sha256:84b7952858e9319968b0348d9894a91a6bb5f31e81a45c68044d040a12362abe"}, + {file = "boto3-1.28.15.tar.gz", hash = "sha256:a6e711e0b6960c3a5b789bd30c5a18eea7263f2a59fc07f85efa5e04804e49d2"}, +] + +[package.dependencies] +botocore = ">=1.31.15,<1.32.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.31.15" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">= 3.7" +files = [ + {file = "botocore-1.31.15-py3-none-any.whl", hash = "sha256:b3a0f787f275711875476cbe12a0123b2e6570b2f505e2fa509dcec3c5410b57"}, + {file = "botocore-1.31.15.tar.gz", hash = "sha256:b46d1ce4e0cf42d28fdf61ce0c999904645d38b51cb809817a361c0cec16d487"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.16.26)"] + +[[package]] +name = "cachetools" +version = "5.3.1" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "click" +version = "8.1.6" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "codespell" +version = "2.2.5" +description = "Codespell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "codespell-2.2.5-py3-none-any.whl", hash = "sha256:efa037f54b73c84f7bd14ce8e853d5f822cdd6386ef0ff32e957a3919435b9ec"}, + {file = "codespell-2.2.5.tar.gz", hash = "sha256:6d9faddf6eedb692bf80c9a94ec13ab4f5fb585aabae5f3750727148d7b5be56"}, +] + +[package.extras] +dev = ["Pygments", "build", "chardet", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli"] +hard-encoding-detection = ["chardet"] +toml = ["tomli"] +types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cosl" +version = "0.0.5" +description = "Utils for COS Lite charms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cosl-0.0.5-py3-none-any.whl", hash = "sha256:84666fde29b792299827d65a1b9b2e3c56029c769e892c8244b50ce793458894"}, + {file = "cosl-0.0.5.tar.gz", hash = "sha256:31c131d1f04c061d3fbef49a4e0a175d4cb481deeb06d0cb3c7b242e4c5416be"}, +] + +[package.dependencies] +ops = "*" +pyyaml = "*" +typing-extensions = "*" + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "41.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, + {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, + {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, + {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = "*" +files = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, +] + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + +[[package]] +name = "flake8" +version = "6.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, + {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.10.0,<2.11.0" +pyflakes = ">=3.0.0,<3.1.0" + +[[package]] +name = "flake8-builtins" +version = "2.1.0" +description = "Check for python builtins being used as variables or parameters." +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8-builtins-2.1.0.tar.gz", hash = "sha256:12ff1ee96dd4e1f3141141ee6c45a5c7d3b3c440d0949e9b8d345c42b39c51d4"}, + {file = "flake8_builtins-2.1.0-py3-none-any.whl", hash = "sha256:469e8f03d6d0edf4b1e62b6d5a97dce4598592c8a13ec8f0952e7a185eba50a1"}, +] + +[package.dependencies] +flake8 = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "flake8-copyright" +version = "0.2.4" +description = "Adds copyright checks to flake8" +optional = false +python-versions = "*" +files = [ + {file = "flake8-copyright-0.2.4.tar.gz", hash = "sha256:b78491fcf575266d7e78dcfa899c876edd1c29929d247de3408bf4e3f971bf1c"}, + {file = "flake8_copyright-0.2.4-py3-none-any.whl", hash = "sha256:5d33d900c4183bb6748692407867229d1e5b84016a100e8899a7f58dcf52223f"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "google-auth" +version = "2.22.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "google-auth-2.22.0.tar.gz", hash = "sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce"}, + {file = "google_auth-2.22.0-py2.py3-none-any.whl", hash = "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" +six = ">=1.9.0" +urllib3 = "<2.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipdb" +version = "0.13.13" +description = "IPython-enabled pdb" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, + {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, +] + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\""} +ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} +tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} + +[[package]] +name = "ipython" +version = "8.14.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, + {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jedi" +version = "0.19.0" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, + {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsonschema" +version = "4.18.4" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.18.4-py3-none-any.whl", hash = "sha256:971be834317c22daaa9132340a51c01b50910724082c2c1a2ac87eeec153a3fe"}, + {file = "jsonschema-4.18.4.tar.gz", hash = "sha256:fb3642735399fa958c0d2aad7057901554596c63349f4f6b283c493cf692a25d"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.7.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, + {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, +] + +[package.dependencies] +referencing = ">=0.28.0" + +[[package]] +name = "juju" +version = "2.9.44.0" +description = "Python library for Juju" +optional = false +python-versions = "*" +files = [ + {file = "juju-2.9.44.0.tar.gz", hash = "sha256:bc71fe0c8fd59ee00f0c3b03066682cd2273f299c36135451abb1a81289e68f9"}, +] + +[package.dependencies] +kubernetes = ">=12.0.1" +macaroonbakery = ">=1.1,<2.0" +paramiko = ">=2.4.0,<3.0.0" +pyasn1 = ">=0.4.4" +pyRFC3339 = ">=1.0,<2.0" +pyyaml = ">=5.1.2" +theblues = ">=0.5.1,<1.0" +toposort = ">=1.5,<2" +typing_inspect = ">=0.6.0" +websockets = {version = ">=9.0", markers = "python_version > \"3.9\""} + +[[package]] +name = "jujubundlelib" +version = "0.5.7" +description = "A python library for working with Juju bundles" +optional = false +python-versions = "*" +files = [ + {file = "jujubundlelib-0.5.7.tar.gz", hash = "sha256:7e2b1a679faab13c4d56256e31e0cc616d55841abd32598951735bf395ca47e3"}, +] + +[package.dependencies] +PyYAML = ">=3.11" + +[[package]] +name = "kubernetes" +version = "27.2.0" +description = "Kubernetes python client" +optional = false +python-versions = ">=3.6" +files = [ + {file = "kubernetes-27.2.0-py2.py3-none-any.whl", hash = "sha256:0f9376329c85cf07615ed6886bf9bf21eb1cbfc05e14ec7b0f74ed8153cd2815"}, + {file = "kubernetes-27.2.0.tar.gz", hash = "sha256:d479931c6f37561dbfdf28fc5f46384b1cb8b28f9db344ed4a232ce91990825a"}, +] + +[package.dependencies] +certifi = ">=14.05.14" +google-auth = ">=1.0.1" +oauthlib = ">=3.2.2" +python-dateutil = ">=2.5.3" +pyyaml = ">=5.4.1" +requests = "*" +requests-oauthlib = "*" +six = ">=1.9.0" +urllib3 = ">=1.24.2" +websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" + +[package.extras] +adal = ["adal (>=1.0.2)"] + +[[package]] +name = "macaroonbakery" +version = "1.3.1" +description = "A Python library port for bakery, higher level operation to work with macaroons" +optional = false +python-versions = "*" +files = [ + {file = "macaroonbakery-1.3.1.tar.gz", hash = "sha256:23f38415341a1d04a155b4dac6730d3ad5f39b86ce07b1bb134bdda52b48b053"}, +] + +[package.dependencies] +protobuf = ">=3.0.0,<4.0" +pymacaroons = ">=0.12.0,<1.0" +PyNaCl = ">=1.1.2,<2.0" +pyRFC3339 = ">=1.0,<2.0" +requests = ">=2.18.1,<3.0" +six = ">=1.11.0,<2.0" + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "mysql-connector-python" +version = "8.0.33" +description = "MySQL driver written in Python" +optional = false +python-versions = "*" +files = [ + {file = "mysql-connector-python-8.0.33.tar.gz", hash = "sha256:9775331fa60b5d5a6925781d77eee4384e2b54a12dea694ffdefd1cf1a9c0fdb"}, + {file = "mysql_connector_python-8.0.33-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:241483065ad062256985e082e3cbb3e7d1d6d2275cee17c66d22525b09096201"}, + {file = "mysql_connector_python-8.0.33-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:9f5eb33e29742c5f8ef23df2d3f0de0e46f4325e4324016e15aba7f8665a68c0"}, + {file = "mysql_connector_python-8.0.33-cp310-cp310-manylinux1_i686.whl", hash = "sha256:4c82fb70f44f2469c0879434c1d8ee3162f56a40cc8f5ca1cc4d97f06c84cd43"}, + {file = "mysql_connector_python-8.0.33-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:016662c6252f2c5f47805d9168187be1316d0c1d7109f9fe668482c3d6e5711d"}, + {file = "mysql_connector_python-8.0.33-cp310-cp310-win_amd64.whl", hash = "sha256:46ff8a10c13f39996d60f45c30cf2ea15e883bc71d58259ed2fea0a5a6fb93a3"}, + {file = "mysql_connector_python-8.0.33-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:db422b19347c5d00e078dd64e281e5b1e5a19a2d972dc2d9733b136d79c34798"}, + {file = "mysql_connector_python-8.0.33-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:37eace5b7eb676a41ff1edc5cf6ce4ae1c28406d1a6fe84941e6aa396d688195"}, + {file = "mysql_connector_python-8.0.33-cp311-cp311-manylinux1_i686.whl", hash = "sha256:753d07fb39a67f7f35fe6e6a4fac12008287661de59f9d5c0bf4da3359d83eb8"}, + {file = "mysql_connector_python-8.0.33-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:984f5649e6abee04461d6f52fbc77387d7137b8fd003c54bac66505006f17183"}, + {file = "mysql_connector_python-8.0.33-cp311-cp311-win_amd64.whl", hash = "sha256:f324233af7ec9fcb19c23096af27662459708c0465886cb017d78ff3f5b78b55"}, + {file = "mysql_connector_python-8.0.33-cp37-cp37m-macosx_12_0_x86_64.whl", hash = "sha256:41db4452a99ee28494313eab1aa7749475d3e39bed7b24a0868aee45bd0d9c73"}, + {file = "mysql_connector_python-8.0.33-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:34d5c5f6ec7c1e75bf972def40d097138e097dc694e36dec89a5dd604ef7aada"}, + {file = "mysql_connector_python-8.0.33-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a632d7b0e569a46e6d44e6cd3f8db747995a787a081870697dbfd3ae18949339"}, + {file = "mysql_connector_python-8.0.33-cp37-cp37m-win_amd64.whl", hash = "sha256:e853e12c00e3beabc581f4e039222708ee606fef80a3bac6b1f497ed89a31aea"}, + {file = "mysql_connector_python-8.0.33-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:96f7fb0ccfe96e6e478e5f0f034c99bda961b99ffa1c746cee39cfea45b0c04d"}, + {file = "mysql_connector_python-8.0.33-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7318f416b9defe84b2bd025304bab62b68f8d8fcbe479af5593161eff12ef169"}, + {file = "mysql_connector_python-8.0.33-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0004426e964856148e1cde31e9b8be63ae3013715b048ff0f2ede69a6ddd36f7"}, + {file = "mysql_connector_python-8.0.33-cp38-cp38-win_amd64.whl", hash = "sha256:d8167868ebad8d78ba69babd028626e96a51365cab76edf735b2559731759b62"}, + {file = "mysql_connector_python-8.0.33-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:f403ff22d3514d08028590fef463d17dc107ac72ea27a49429614949d82fda40"}, + {file = "mysql_connector_python-8.0.33-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:af9feec311d8ea51261e1ef1f959a442708e30f0024d08d0fb537b07a1271634"}, + {file = "mysql_connector_python-8.0.33-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7266d7b2550f9fe0cdcea1647aa6aade352e14095042b6a3921c9152cf8543e8"}, + {file = "mysql_connector_python-8.0.33-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:ea05590cb972b114efa027c343b4b7110d8e8450493984ebfb9a651e27674636"}, + {file = "mysql_connector_python-8.0.33-cp39-cp39-win_amd64.whl", hash = "sha256:3bedf8265fb31698e4144ca54e241e3386802c3a437745b1a536a74cbe7e4fb9"}, + {file = "mysql_connector_python-8.0.33-py2.py3-none-any.whl", hash = "sha256:c20a85a69af41d2d7d5cf52106f0b9473775819d189487c6ff3d3f3946931ca2"}, +] + +[package.dependencies] +protobuf = ">=3.11.0,<=3.20.3" + +[package.extras] +compression = ["lz4 (>=2.1.6,<=4.3.2)", "zstandard (>=0.12.0,<=0.19.0)"] +dns-srv = ["dnspython (>=1.16.0,<=2.3.0)"] +gssapi = ["gssapi (>=1.6.9,<=1.8.2)"] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "ops" +version = "2.4.1" +description = "The Python library behind great charms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ops-2.4.1-py3-none-any.whl", hash = "sha256:9cd1a25c6ae30dc9f9afcbe87250f5349ff9a05d5b06632ad8111700c5f0db04"}, + {file = "ops-2.4.1.tar.gz", hash = "sha256:b5762bea03049ec150c6266833925d0bb36237b26b006ca878e74e24d162f981"}, +] + +[package.dependencies] +PyYAML = "==6.*" +websocket-client = "==1.*" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "paramiko" +version = "2.12.0" +description = "SSH2 protocol library" +optional = false +python-versions = "*" +files = [ + {file = "paramiko-2.12.0-py2.py3-none-any.whl", hash = "sha256:b2df1a6325f6996ef55a8789d0462f5b502ea83b3c990cbb5bbe57345c6812c4"}, + {file = "paramiko-2.12.0.tar.gz", hash = "sha256:376885c05c5d6aa6e1f4608aac2a6b5b0548b1add40274477324605903d9cd49"}, +] + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" +six = "*" + +[package.extras] +all = ["bcrypt (>=3.1.3)", "gssapi (>=1.4.1)", "invoke (>=1.3)", "pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["bcrypt (>=3.1.3)", "pynacl (>=1.0.1)"] +gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "pep8-naming" +version = "0.13.3" +description = "Check PEP-8 naming conventions, plugin for flake8" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"}, + {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"}, +] + +[package.dependencies] +flake8 = ">=5.0.0" + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.39" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "protobuf" +version = "3.20.3" +description = "Protocol Buffers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99"}, + {file = "protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e"}, + {file = "protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c"}, + {file = "protobuf-3.20.3-cp310-cp310-win_amd64.whl", hash = "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7"}, + {file = "protobuf-3.20.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469"}, + {file = "protobuf-3.20.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4"}, + {file = "protobuf-3.20.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4"}, + {file = "protobuf-3.20.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454"}, + {file = "protobuf-3.20.3-cp37-cp37m-win32.whl", hash = "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905"}, + {file = "protobuf-3.20.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c"}, + {file = "protobuf-3.20.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7"}, + {file = "protobuf-3.20.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"}, + {file = "protobuf-3.20.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050"}, + {file = "protobuf-3.20.3-cp38-cp38-win32.whl", hash = "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86"}, + {file = "protobuf-3.20.3-cp38-cp38-win_amd64.whl", hash = "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9"}, + {file = "protobuf-3.20.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b"}, + {file = "protobuf-3.20.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b"}, + {file = "protobuf-3.20.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402"}, + {file = "protobuf-3.20.3-cp39-cp39-win32.whl", hash = "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480"}, + {file = "protobuf-3.20.3-cp39-cp39-win_amd64.whl", hash = "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7"}, + {file = "protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db"}, + {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pyasn1" +version = "0.5.0" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, + {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.3.0" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.6.0" + +[[package]] +name = "pycodestyle" +version = "2.10.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, + {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "1.10.12" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, + {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, + {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, + {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, + {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, + {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, + {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, + {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, + {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, + {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pyflakes" +version = "3.0.1" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, + {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, +] + +[[package]] +name = "pygments" +version = "2.15.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, + {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymacaroons" +version = "0.13.0" +description = "Macaroon library for Python" +optional = false +python-versions = "*" +files = [ + {file = "pymacaroons-0.13.0-py2.py3-none-any.whl", hash = "sha256:3e14dff6a262fdbf1a15e769ce635a8aea72e6f8f91e408f9a97166c53b91907"}, + {file = "pymacaroons-0.13.0.tar.gz", hash = "sha256:1e6bba42a5f66c245adf38a5a4006a99dcc06a0703786ea636098667d42903b8"}, +] + +[package.dependencies] +PyNaCl = ">=1.1.2,<2.0" +six = ">=1.8.0" + +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + +[[package]] +name = "pyopenssl" +version = "23.2.0" +description = "Python wrapper module around the OpenSSL library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyOpenSSL-23.2.0-py3-none-any.whl", hash = "sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2"}, + {file = "pyOpenSSL-23.2.0.tar.gz", hash = "sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac"}, +] + +[package.dependencies] +cryptography = ">=38.0.0,<40.0.0 || >40.0.0,<40.0.1 || >40.0.1,<42" + +[package.extras] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] +test = ["flaky", "pretend", "pytest (>=3.0.1)"] + +[[package]] +name = "pyproject-flake8" +version = "6.0.0.post1" +description = "pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "pyproject-flake8-6.0.0.post1.tar.gz", hash = "sha256:d43421caca0ef8a672874405fe63c722b0333e3c22c41648c6df60f21bab2b6b"}, + {file = "pyproject_flake8-6.0.0.post1-py3-none-any.whl", hash = "sha256:bdc7ca9b967b9724983903489b8943b72c668178fb69f03e8774ec74f6a13782"}, +] + +[package.dependencies] +flake8 = "6.0.0" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[[package]] +name = "pyrfc3339" +version = "1.1" +description = "Generate and parse RFC 3339 timestamps" +optional = false +python-versions = "*" +files = [ + {file = "pyRFC3339-1.1-py2.py3-none-any.whl", hash = "sha256:67196cb83b470709c580bb4738b83165e67c6cc60e1f2e4f286cfcb402a926f4"}, + {file = "pyRFC3339-1.1.tar.gz", hash = "sha256:81b8cbe1519cdb79bed04910dd6fa4e181faf8c88dff1e1b987b5f7ab23a5b1a"}, +] + +[package.dependencies] +pytz = "*" + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-operator" +version = "0.28.0" +description = "Fixtures for Operators" +optional = false +python-versions = "*" +files = [ + {file = "pytest-operator-0.28.0.tar.gz", hash = "sha256:efac98697da71558790eb5d4c9d42f11f3d5fb43dff22a802aee69e1801edce8"}, + {file = "pytest_operator-0.28.0-py3-none-any.whl", hash = "sha256:b3cb5a8ebf838f890133a25ee520c25c8be259b54341e42e39f64a6d97735d9f"}, +] + +[package.dependencies] +ipdb = "*" +jinja2 = "*" +juju = "*" +pytest = "*" +pytest-asyncio = "*" +pyyaml = "*" + +[[package]] +name = "pytest-operator-cache" +version = "0.1.0" +description = "" +optional = false +python-versions = "^3.10" +files = [] +develop = false + +[package.dependencies] +pyyaml = "*" + +[package.source] +type = "git" +url = "https://github.com/canonical/data-platform-workflows" +reference = "v4.1.0" +resolved_reference = "33f562c6c5702121cd4d779a1e333fe63a9e1d4c" +subdirectory = "python/pytest_plugins/pytest_operator_cache" + +[[package]] +name = "pytest-operator-groups" +version = "0.1.0" +description = "" +optional = false +python-versions = "^3.10" +files = [] +develop = false + +[package.dependencies] +pytest = "*" + +[package.source] +type = "git" +url = "https://github.com/canonical/data-platform-workflows" +reference = "v4.1.0" +resolved_reference = "33f562c6c5702121cd4d779a1e333fe63a9e1d4c" +subdirectory = "python/pytest_plugins/pytest_operator_groups" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "referencing" +version = "0.30.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.30.0-py3-none-any.whl", hash = "sha256:c257b08a399b6c2f5a3510a50d28ab5dbc7bbde049bcaf954d43c446f83ab548"}, + {file = "referencing-0.30.0.tar.gz", hash = "sha256:47237742e990457f7512c7d27486394a9aadaf876cbfaa4be65b27b4f4d47c6b"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "rpds-py" +version = "0.9.2" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.9.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:ab6919a09c055c9b092798ce18c6c4adf49d24d4d9e43a92b257e3f2548231e7"}, + {file = "rpds_py-0.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d55777a80f78dd09410bd84ff8c95ee05519f41113b2df90a69622f5540c4f8b"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a216b26e5af0a8e265d4efd65d3bcec5fba6b26909014effe20cd302fd1138fa"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29cd8bfb2d716366a035913ced99188a79b623a3512292963d84d3e06e63b496"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44659b1f326214950a8204a248ca6199535e73a694be8d3e0e869f820767f12f"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:745f5a43fdd7d6d25a53ab1a99979e7f8ea419dfefebcab0a5a1e9095490ee5e"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a987578ac5214f18b99d1f2a3851cba5b09f4a689818a106c23dbad0dfeb760f"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf4151acb541b6e895354f6ff9ac06995ad9e4175cbc6d30aaed08856558201f"}, + {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:03421628f0dc10a4119d714a17f646e2837126a25ac7a256bdf7c3943400f67f"}, + {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13b602dc3e8dff3063734f02dcf05111e887f301fdda74151a93dbbc249930fe"}, + {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fae5cb554b604b3f9e2c608241b5d8d303e410d7dfb6d397c335f983495ce7f6"}, + {file = "rpds_py-0.9.2-cp310-none-win32.whl", hash = "sha256:47c5f58a8e0c2c920cc7783113df2fc4ff12bf3a411d985012f145e9242a2764"}, + {file = "rpds_py-0.9.2-cp310-none-win_amd64.whl", hash = "sha256:4ea6b73c22d8182dff91155af018b11aac9ff7eca085750455c5990cb1cfae6e"}, + {file = "rpds_py-0.9.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:e564d2238512c5ef5e9d79338ab77f1cbbda6c2d541ad41b2af445fb200385e3"}, + {file = "rpds_py-0.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f411330a6376fb50e5b7a3e66894e4a39e60ca2e17dce258d53768fea06a37bd"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e7521f5af0233e89939ad626b15278c71b69dc1dfccaa7b97bd4cdf96536bb7"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d3335c03100a073883857e91db9f2e0ef8a1cf42dc0369cbb9151c149dbbc1b"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d25b1c1096ef0447355f7293fbe9ad740f7c47ae032c2884113f8e87660d8f6e"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a5d3fbd02efd9cf6a8ffc2f17b53a33542f6b154e88dd7b42ef4a4c0700fdad"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5934e2833afeaf36bd1eadb57256239785f5af0220ed8d21c2896ec4d3a765f"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:095b460e117685867d45548fbd8598a8d9999227e9061ee7f012d9d264e6048d"}, + {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91378d9f4151adc223d584489591dbb79f78814c0734a7c3bfa9c9e09978121c"}, + {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24a81c177379300220e907e9b864107614b144f6c2a15ed5c3450e19cf536fae"}, + {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de0b6eceb46141984671802d412568d22c6bacc9b230174f9e55fc72ef4f57de"}, + {file = "rpds_py-0.9.2-cp311-none-win32.whl", hash = "sha256:700375326ed641f3d9d32060a91513ad668bcb7e2cffb18415c399acb25de2ab"}, + {file = "rpds_py-0.9.2-cp311-none-win_amd64.whl", hash = "sha256:0766babfcf941db8607bdaf82569ec38107dbb03c7f0b72604a0b346b6eb3298"}, + {file = "rpds_py-0.9.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1440c291db3f98a914e1afd9d6541e8fc60b4c3aab1a9008d03da4651e67386"}, + {file = "rpds_py-0.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f2996fbac8e0b77fd67102becb9229986396e051f33dbceada3debaacc7033f"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f30d205755566a25f2ae0382944fcae2f350500ae4df4e795efa9e850821d82"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:159fba751a1e6b1c69244e23ba6c28f879a8758a3e992ed056d86d74a194a0f3"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1f044792e1adcea82468a72310c66a7f08728d72a244730d14880cd1dabe36b"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9251eb8aa82e6cf88510530b29eef4fac825a2b709baf5b94a6094894f252387"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01899794b654e616c8625b194ddd1e5b51ef5b60ed61baa7a2d9c2ad7b2a4238"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0c43f8ae8f6be1d605b0465671124aa8d6a0e40f1fb81dcea28b7e3d87ca1e1"}, + {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207f57c402d1f8712618f737356e4b6f35253b6d20a324d9a47cb9f38ee43a6b"}, + {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b52e7c5ae35b00566d244ffefba0f46bb6bec749a50412acf42b1c3f402e2c90"}, + {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:978fa96dbb005d599ec4fd9ed301b1cc45f1a8f7982d4793faf20b404b56677d"}, + {file = "rpds_py-0.9.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6aa8326a4a608e1c28da191edd7c924dff445251b94653988efb059b16577a4d"}, + {file = "rpds_py-0.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aad51239bee6bff6823bbbdc8ad85136c6125542bbc609e035ab98ca1e32a192"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd4dc3602370679c2dfb818d9c97b1137d4dd412230cfecd3c66a1bf388a196"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd9da77c6ec1f258387957b754f0df60766ac23ed698b61941ba9acccd3284d1"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:190ca6f55042ea4649ed19c9093a9be9d63cd8a97880106747d7147f88a49d18"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:876bf9ed62323bc7dcfc261dbc5572c996ef26fe6406b0ff985cbcf460fc8a4c"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa2818759aba55df50592ecbc95ebcdc99917fa7b55cc6796235b04193eb3c55"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ea4d00850ef1e917815e59b078ecb338f6a8efda23369677c54a5825dbebb55"}, + {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5855c85eb8b8a968a74dc7fb014c9166a05e7e7a8377fb91d78512900aadd13d"}, + {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:14c408e9d1a80dcb45c05a5149e5961aadb912fff42ca1dd9b68c0044904eb32"}, + {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:65a0583c43d9f22cb2130c7b110e695fff834fd5e832a776a107197e59a1898e"}, + {file = "rpds_py-0.9.2-cp38-none-win32.whl", hash = "sha256:71f2f7715935a61fa3e4ae91d91b67e571aeb5cb5d10331ab681256bda2ad920"}, + {file = "rpds_py-0.9.2-cp38-none-win_amd64.whl", hash = "sha256:674c704605092e3ebbbd13687b09c9f78c362a4bc710343efe37a91457123044"}, + {file = "rpds_py-0.9.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:07e2c54bef6838fa44c48dfbc8234e8e2466d851124b551fc4e07a1cfeb37260"}, + {file = "rpds_py-0.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fdf55283ad38c33e35e2855565361f4bf0abd02470b8ab28d499c663bc5d7c"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:890ba852c16ace6ed9f90e8670f2c1c178d96510a21b06d2fa12d8783a905193"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50025635ba8b629a86d9d5474e650da304cb46bbb4d18690532dd79341467846"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517cbf6e67ae3623c5127206489d69eb2bdb27239a3c3cc559350ef52a3bbf0b"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0836d71ca19071090d524739420a61580f3f894618d10b666cf3d9a1688355b1"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c439fd54b2b9053717cca3de9583be6584b384d88d045f97d409f0ca867d80f"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f68996a3b3dc9335037f82754f9cdbe3a95db42bde571d8c3be26cc6245f2324"}, + {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7d68dc8acded354c972116f59b5eb2e5864432948e098c19fe6994926d8e15c3"}, + {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f963c6b1218b96db85fc37a9f0851eaf8b9040aa46dec112611697a7023da535"}, + {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a46859d7f947061b4010e554ccd1791467d1b1759f2dc2ec9055fa239f1bc26"}, + {file = "rpds_py-0.9.2-cp39-none-win32.whl", hash = "sha256:e07e5dbf8a83c66783a9fe2d4566968ea8c161199680e8ad38d53e075df5f0d0"}, + {file = "rpds_py-0.9.2-cp39-none-win_amd64.whl", hash = "sha256:682726178138ea45a0766907957b60f3a1bf3acdf212436be9733f28b6c5af3c"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:196cb208825a8b9c8fc360dc0f87993b8b260038615230242bf18ec84447c08d"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c7671d45530fcb6d5e22fd40c97e1e1e01965fc298cbda523bb640f3d923b387"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83b32f0940adec65099f3b1c215ef7f1d025d13ff947975a055989cb7fd019a4"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f67da97f5b9eac838b6980fc6da268622e91f8960e083a34533ca710bec8611"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03975db5f103997904c37e804e5f340c8fdabbb5883f26ee50a255d664eed58c"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:987b06d1cdb28f88a42e4fb8a87f094e43f3c435ed8e486533aea0bf2e53d931"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c861a7e4aef15ff91233751619ce3a3d2b9e5877e0fcd76f9ea4f6847183aa16"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02938432352359805b6da099c9c95c8a0547fe4b274ce8f1a91677401bb9a45f"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef1f08f2a924837e112cba2953e15aacfccbbfcd773b4b9b4723f8f2ddded08e"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:35da5cc5cb37c04c4ee03128ad59b8c3941a1e5cd398d78c37f716f32a9b7f67"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:141acb9d4ccc04e704e5992d35472f78c35af047fa0cfae2923835d153f091be"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79f594919d2c1a0cc17d1988a6adaf9a2f000d2e1048f71f298b056b1018e872"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a06418fe1155e72e16dddc68bb3780ae44cebb2912fbd8bb6ff9161de56e1798"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2eb034c94b0b96d5eddb290b7b5198460e2d5d0c421751713953a9c4e47d10"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b08605d248b974eb02f40bdcd1a35d3924c83a2a5e8f5d0fa5af852c4d960af"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0805911caedfe2736935250be5008b261f10a729a303f676d3d5fea6900c96a"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab2299e3f92aa5417d5e16bb45bb4586171c1327568f638e8453c9f8d9e0f020"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c8d7594e38cf98d8a7df25b440f684b510cf4627fe038c297a87496d10a174f"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b9ec12ad5f0a4625db34db7e0005be2632c1013b253a4a60e8302ad4d462afd"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1fcdee18fea97238ed17ab6478c66b2095e4ae7177e35fb71fbe561a27adf620"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:933a7d5cd4b84f959aedeb84f2030f0a01d63ae6cf256629af3081cf3e3426e8"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:686ba516e02db6d6f8c279d1641f7067ebb5dc58b1d0536c4aaebb7bf01cdc5d"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0173c0444bec0a3d7d848eaeca2d8bd32a1b43f3d3fde6617aac3731fa4be05f"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d576c3ef8c7b2d560e301eb33891d1944d965a4d7a2eacb6332eee8a71827db6"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed89861ee8c8c47d6beb742a602f912b1bb64f598b1e2f3d758948721d44d468"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1054a08e818f8e18910f1bee731583fe8f899b0a0a5044c6e680ceea34f93876"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e7c4bb27ff1aab90dcc3e9d37ee5af0231ed98d99cb6f5250de28889a3d502"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c545d9d14d47be716495076b659db179206e3fd997769bc01e2d550eeb685596"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9039a11bca3c41be5a58282ed81ae422fa680409022b996032a43badef2a3752"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb39aca7a64ad0c9490adfa719dbeeb87d13be137ca189d2564e596f8ba32c07"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2d8b3b3a2ce0eaa00c5bbbb60b6713e94e7e0becab7b3db6c5c77f979e8ed1f1"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:99b1c16f732b3a9971406fbfe18468592c5a3529585a45a35adbc1389a529a03"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c27ee01a6c3223025f4badd533bea5e87c988cb0ba2811b690395dfe16088cfe"}, + {file = "rpds_py-0.9.2.tar.gz", hash = "sha256:8d70e8f14900f2657c249ea4def963bed86a29b81f81f5b76b5a9215680de945"}, +] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "s3transfer" +version = "0.6.1" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, + {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, +] + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "shellcheck-py" +version = "0.9.0.5" +description = "Python wrapper around invoking shellcheck (https://www.shellcheck.net/)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellcheck_py-0.9.0.5-py2.py3-none-macosx_10_15_x86_64.whl", hash = "sha256:98d9668f72afeb65c7a8e60f02202b00d64f2de9e9b103dfb5d0067ded391ef3"}, + {file = "shellcheck_py-0.9.0.5-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65ddc19a1ae4249802a663682834ed452f9e75615d58c3ce6b3f1b0d2a484f32"}, + {file = "shellcheck_py-0.9.0.5-py2.py3-none-win_amd64.whl", hash = "sha256:9f50a7354f355753f365668e79aa3d410cb6f4d9358e4c5d8464018cf2b4863a"}, + {file = "shellcheck_py-0.9.0.5.tar.gz", hash = "sha256:50b2057fac7227fd83614a9bf9d123042e53e03d92f2c7f1778448a8937f07a4"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "tenacity" +version = "8.2.2" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.6" +files = [ + {file = "tenacity-8.2.2-py3-none-any.whl", hash = "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0"}, + {file = "tenacity-8.2.2.tar.gz", hash = "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "theblues" +version = "0.5.2" +description = "Python library for using the juju charm store API." +optional = false +python-versions = "*" +files = [ + {file = "theblues-0.5.2.tar.gz", hash = "sha256:a9aded6b151c67d83eb9adcbcb38640872d9f29db985053259afd2fc012e5ed9"}, +] + +[package.dependencies] +jujubundlelib = ">=0.5.1" +macaroonbakery = ">=0.0.6" +requests = ">=2.18.4" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "toposort" +version = "1.10" +description = "Implements a topological sort algorithm." +optional = false +python-versions = "*" +files = [ + {file = "toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87"}, + {file = "toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd"}, +] + +[[package]] +name = "traitlets" +version = "5.9.0" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.7" +files = [ + {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, + {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +description = "Runtime inspection utilities for typing module." +optional = false +python-versions = "*" +files = [ + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, +] + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + +[[package]] +name = "websocket-client" +version = "1.6.1" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"}, + {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"}, +] + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "websockets" +version = "11.0.3" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, + {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, + {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, + {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, + {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, + {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, + {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, + {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, + {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, + {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, + {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, + {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, + {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, + {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "98c872469ec49350758da8ee699638f54a7278a199ea92b8a2cfdfe1328bfe82" diff --git a/pyproject.toml b/pyproject.toml index f70ee5546..1d1c755dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,70 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -# Testing tools configuration +[tool.poetry] +# Charm is not packed as a standard Python package; this information is not used +name = "charm" +version = "0.1.0" +description = "" +authors = [] + +[tool.poetry.dependencies] +python = "^3.10" +ops = "^2.4.1" +tenacity = "^8.2.2" +boto3 = "^1.28.7" +# TODO: Remove insecure dependency https://github.com/canonical/mysql-operator/issues/246 +pyopenssl = "^23.2.0" + +[tool.poetry.group.charm-libs.dependencies] +# data_platform_libs/v0/data_interfaces.py +ops = ">=2.0.0" +# grafana_agent/v0/cos_agent.py +cosl = "*" +pydantic = "<2" +# tls_certificates_interface/v1/tls_certificates.py +cryptography = "*" +jsonschema = "*" + +[tool.poetry.group.format] +optional = true + +[tool.poetry.group.format.dependencies] +black = "^23.7.0" +isort = "^5.12.0" + +[tool.poetry.group.lint] +optional = true + +[tool.poetry.group.lint.dependencies] +black = "^23.7.0" +isort = "^5.12.0" +flake8 = "^6.0.0" +flake8-docstrings = "^1.7.0" +flake8-copyright = "^0.2.4" +flake8-builtins = "^2.1.0" +pyproject-flake8 = "^6.0.0.post1" +pep8-naming = "^0.13.3" +codespell = "^2.2.5" +shellcheck-py = "^0.9.0.5" + +[tool.poetry.group.unit.dependencies] +pytest = "^7.4.0" +coverage = {extras = ["toml"], version = "^7.2.7"} + +[tool.poetry.group.integration.dependencies] +pytest = "^7.4.0" +pytest-operator = "^0.28.0" +juju = "^2.9.44.0" +mysql-connector-python = "~8.0.33" +tenacity = "^8.2.2" +boto3 = "^1.28.11" +pyyaml = "^6.0" +urllib3 = "^1.26.16" +pytest-operator-cache = {git = "https://github.com/canonical/data-platform-workflows", tag = "v4.1.0", subdirectory = "python/pytest_plugins/pytest_operator_cache"} +pytest-operator-groups = {git = "https://github.com/canonical/data-platform-workflows", tag = "v4.1.0", subdirectory = "python/pytest_plugins/pytest_operator_groups"} + + [tool.coverage.run] branch = true @@ -11,7 +74,7 @@ show_missing = true [tool.pytest.ini_options] minversion = "6.0" log_cli_level = "INFO" -markers = ["group", "unstable"] +markers = ["unstable"] # Formatting tools configuration [tool.black] diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 9395be47e..000000000 --- a/renovate.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ], - "reviewers": ["team:data-platform-mysql"], - "enabledManagers": ["poetry", "pip_requirements", "github-actions", "regex"], - "schedule": ["after 1am and before 2am every weekday"], - "timezone": "Etc/UTC", - "prHourlyLimit": 0, - "packageRules": [ - { - "matchManagers": ["poetry", "pip_requirements"], - "groupName": "Python dependencies" - }, { - "matchManagers": ["github-actions"], - "groupName": "GitHub actions" - }, { - "matchPackageNames": ["juju/juju"], - "allowedVersions": "<3.0.0", - "extractVersion": "^juju-(?.*)$", - "groupName": "Juju agent" - } - ], - "regexManagers": [ - { - "fileMatch": ["^(workflow-templates|\\.github/workflows)/[^/]+\\.ya?ml$"], - "matchStrings": ["\"--agent-version[= ](?.*?)\" +# renovate: latest"], - "depNameTemplate": "juju/juju", - "datasourceTemplate": "github-releases", - "versioningTemplate": "loose", - "extractVersionTemplate": "Juju release" - } - ], - "ignoreDeps": [] -} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 96133b3fd..000000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -boto3==1.26.141 -cosl==0.0.5 -cryptography==40.0.2 -jsonschema==4.17.3 -ops >= 2.0.0 -pydantic==1.10.8 -pyOpenSSL >= "23.0.0" -tenacity==8.2.2 -urllib3==1.26.16 diff --git a/src/charm.py b/src/charm.py index 7e61167fa..292f1adc1 100755 --- a/src/charm.py +++ b/src/charm.py @@ -6,13 +6,15 @@ import logging import subprocess -from typing import Dict, Optional +from typing import Optional +import tenacity from charms.data_platform_libs.v0.s3 import S3Requirer from charms.grafana_agent.v0.cos_agent import COSAgentProvider from charms.mysql.v0.backups import MySQLBackups from charms.mysql.v0.mysql import ( MySQLAddInstanceToClusterError, + MySQLCharmBase, MySQLConfigureInstanceError, MySQLConfigureMySQLUsersError, MySQLCreateClusterError, @@ -27,10 +29,10 @@ ) from charms.mysql.v0.tls import MySQLTLS from ops.charm import ( - ActionEvent, - CharmBase, InstallEvent, + RelationBrokenEvent, RelationChangedEvent, + RelationCreatedEvent, StartEvent, ) from ops.main import main @@ -51,30 +53,30 @@ CHARMED_MYSQLD_SERVICE, CLUSTER_ADMIN_PASSWORD_KEY, CLUSTER_ADMIN_USERNAME, + COS_AGENT_RELATION_NAME, GR_MAX_MEMBERS, MONITORING_PASSWORD_KEY, MONITORING_USERNAME, MYSQL_EXPORTER_PORT, PASSWORD_LENGTH, PEER, - REQUIRED_USERNAMES, ROOT_PASSWORD_KEY, - ROOT_USERNAME, S3_INTEGRATOR_RELATION_NAME, SERVER_CONFIG_PASSWORD_KEY, SERVER_CONFIG_USERNAME, ) +from hostname_resolution import MySQLMachineHostnameResolution from mysql_vm_helpers import ( MySQL, MySQLCreateCustomMySQLDConfigError, MySQLDataPurgeError, - MySQLExporterConnectError, MySQLReconfigureError, MySQLResetRootPasswordAndStartMySQLDError, SnapServiceOperationError, instance_hostname, is_volume_mounted, reboot_system, + snap, snap_service_operation, ) from relations.db_router import DBRouterRelation @@ -86,7 +88,7 @@ logger = logging.getLogger(__name__) -class MySQLOperatorCharm(CharmBase): +class MySQLOperatorCharm(MySQLCharmBase): """Operator framework charm for MySQL.""" def __init__(self, *args): @@ -102,9 +104,6 @@ def __init__(self, *args): ) self.framework.observe(self.on[PEER].relation_changed, self._on_peer_relation_changed) - self.framework.observe(self.on.get_cluster_status_action, self._get_cluster_status) - self.framework.observe(self.on.get_password_action, self._on_get_password) - self.framework.observe(self.on.set_password_action, self._on_set_password) self.shared_db_relation = SharedDBRelation(self) self.db_router_relation = DBRouterRelation(self) @@ -120,8 +119,15 @@ def __init__(self, *args): logs_rules_dir="./src/alert_rules/loki", log_slots=[f"{CHARMED_MYSQL_SNAP_NAME}:logs"], ) + self.framework.observe( + self.on[COS_AGENT_RELATION_NAME].relation_created, self._on_cos_agent_relation_created + ) + self.framework.observe( + self.on[COS_AGENT_RELATION_NAME].relation_broken, self._on_cos_agent_relation_broken + ) self.s3_integrator = S3Requirer(self, S3_INTEGRATOR_RELATION_NAME) self.backups = MySQLBackups(self, self.s3_integrator) + self.hostname_resolution = MySQLMachineHostnameResolution(self) # ======================= # Charm Lifecycle Hooks @@ -145,6 +151,7 @@ def set_retry_status(_): for attempt in Retrying( wait=wait_exponential(multiplier=10), stop=stop_after_delay(60 * 5), + retry=tenacity.retry_if_exception_type(snap.SnapError), after=set_retry_status, ): with attempt: @@ -206,9 +213,6 @@ def _on_start(self, event: StartEvent) -> None: except MySQLCreateCustomMySQLDConfigError: self.unit.status = BlockedStatus("Failed to create custom mysqld config") return - except MySQLExporterConnectError: - self.unit.status = BlockedStatus("Failed to connect to MySQL exporter") - return except MySQLGetMySQLVersionError: logger.debug("Fail to get MySQL version") @@ -376,91 +380,27 @@ def _on_update_status(self, _) -> None: # Set active status when primary is known self.app.status = ActiveStatus() - if self._mysql.are_locks_acquired(from_instance=primary_address): - logger.debug("Skip cluster rescan while locks are held") - return - - # Only rescan cluster when topology is not changing - self._mysql.rescan_cluster(remove_instances=True, add_instances=True) - - # ======================= - # Custom Action Handlers - # ======================= - def _get_cluster_status(self, event: ActionEvent) -> None: - """Action used to retrieve the cluster status.""" - status = self._mysql.get_cluster_status() - if status: - event.set_results( - { - "success": True, - "status": status, - } - ) - else: - event.set_results( - { - "success": False, - "message": "Failed to read cluster status. See logs for more information.", - } - ) - - def _on_get_password(self, event: ActionEvent) -> None: - """Action used to retrieve the system user's password.""" - username = event.params.get("username") or ROOT_USERNAME + def _on_cos_agent_relation_created(self, event: RelationCreatedEvent) -> None: + """Handle the cos_agent relation created event. - if username not in REQUIRED_USERNAMES: - event.fail( - f"The action can be run only for users used by the charm: {', '.join(REQUIRED_USERNAMES)} not {username}" - ) + Enable the mysqld-exporter snap service. + """ + if not self._is_peer_data_set: + logger.debug("Charm not yet set up. Deferring") + event.defer() return - if username == ROOT_USERNAME: - secret_key = ROOT_PASSWORD_KEY - elif username == SERVER_CONFIG_USERNAME: - secret_key = SERVER_CONFIG_PASSWORD_KEY - elif username == CLUSTER_ADMIN_USERNAME: - secret_key = CLUSTER_ADMIN_PASSWORD_KEY - elif username == MONITORING_USERNAME: - secret_key = MONITORING_PASSWORD_KEY - elif username == BACKUPS_USERNAME: - secret_key = BACKUPS_PASSWORD_KEY - else: - raise RuntimeError("Invalid username.") - - event.set_results({"username": username, "password": self.get_secret("app", secret_key)}) - - def _on_set_password(self, event: ActionEvent) -> None: - """Action used to update/rotate the system user's password.""" - if not self.unit.is_leader(): - event.fail("set-password action can only be run on the leader unit.") - return + self._mysql.connect_mysql_exporter() - username = event.params.get("username") or ROOT_USERNAME + def _on_cos_agent_relation_broken(self, event: RelationBrokenEvent) -> None: + """Handle the cos_agent relation broken event. - if username not in REQUIRED_USERNAMES: - event.fail( - f"The action can be run only for users used by the charm: {', '.join(REQUIRED_USERNAMES)} not {username}" - ) + Disable the mysqld-exporter snap service. + """ + if not self._is_peer_data_set: return - if username == ROOT_USERNAME: - secret_key = ROOT_PASSWORD_KEY - elif username == SERVER_CONFIG_USERNAME: - secret_key = SERVER_CONFIG_PASSWORD_KEY - elif username == CLUSTER_ADMIN_USERNAME: - secret_key = CLUSTER_ADMIN_PASSWORD_KEY - elif username == MONITORING_USERNAME: - secret_key = MONITORING_PASSWORD_KEY - elif username == BACKUPS_USERNAME: - secret_key = BACKUPS_PASSWORD_KEY - else: - raise RuntimeError("Invalid username.") - - new_password = event.params.get("password") or generate_random_password(PASSWORD_LENGTH) - - self._mysql.update_user_password(username, new_password) - - self.set_secret("app", secret_key, new_password) + self._mysql.stop_mysql_exporter() # ======================= # Helpers @@ -484,42 +424,6 @@ def _mysql(self): self.get_secret("app", BACKUPS_PASSWORD_KEY), ) - @property - def peers(self): - """Retrieve the peer relation (`ops.model.Relation`).""" - return self.model.get_relation(PEER) - - @property - def _is_peer_data_set(self): - """Returns True if the peer relation data is set.""" - return ( - self.app_peer_data.get("cluster-name") - and self.get_secret("app", ROOT_PASSWORD_KEY) - and self.get_secret("app", SERVER_CONFIG_PASSWORD_KEY) - and self.get_secret("app", CLUSTER_ADMIN_PASSWORD_KEY) - ) - - @property - def cluster_initialized(self): - """Returns True if the cluster is initialized.""" - return self.app_peer_data.get("units-added-to-cluster", "0") >= "1" - - @property - def app_peer_data(self) -> Dict: - """Application peer relation data object.""" - if self.peers is None: - return {} - - return self.peers.data[self.app] - - @property - def unit_peer_data(self) -> Dict: - """Unit peer relation data object.""" - if self.peers is None: - return {} - - return self.peers.data[self.unit] - @property def _has_blocked_status(self) -> bool: """Returns whether the unit is in a blocked state.""" @@ -534,30 +438,6 @@ def is_unit_busy(self) -> bool: """Returns whether the unit is in blocked state and should not run any operations.""" return self.unit_peer_data.get("member-state") == "waiting" - def get_secret(self, scope: str, key: str) -> Optional[str]: - """Get secret from the secret storage.""" - if scope == "unit": - return self.unit_peer_data.get(key, None) - elif scope == "app": - return self.app_peer_data.get(key, None) - else: - raise RuntimeError("Unknown secret scope.") - - def set_secret(self, scope: str, key: str, value: Optional[str]) -> None: - """Set secret in the secret storage.""" - if scope == "unit": - if not value: - del self.unit_peer_data[key] - return - self.unit_peer_data.update({key: value}) - elif scope == "app": - if not value: - del self.app_peer_data[key] - return - self.app_peer_data.update({key: value}) - else: - raise RuntimeError("Unknown secret scope.") - def get_unit_hostname(self, unit_name: Optional[str] = None) -> str: """Get the hostname of the unit.""" if unit_name: @@ -578,12 +458,11 @@ def _workload_initialise(self) -> None: Create users and configuration to setup instance as an Group Replication node. Raised errors must be treated on handlers. """ - self._mysql.create_custom_mysqld_config() + self._mysql.create_custom_mysqld_config(profile=self.config["profile"]) self._mysql.reset_root_password_and_start_mysqld() self._mysql.configure_mysql_users() self._mysql.configure_instance() self._mysql.wait_until_mysql_connection() - self._mysql.connect_mysql_exporter() self.unit_peer_data["unit-configured"] = "True" self.unit_peer_data["instance-hostname"] = f"{instance_hostname()}:3306" if workload_version := self._mysql.get_mysql_version(): diff --git a/src/constants.py b/src/constants.py index 23d7681e2..c102cf018 100644 --- a/src/constants.py +++ b/src/constants.py @@ -19,20 +19,13 @@ CLUSTER_ADMIN_PASSWORD_KEY = "cluster-admin-password" MONITORING_PASSWORD_KEY = "monitoring-password" BACKUPS_PASSWORD_KEY = "backups-password" -REQUIRED_USERNAMES = [ - ROOT_USERNAME, - SERVER_CONFIG_USERNAME, - CLUSTER_ADMIN_USERNAME, - MONITORING_USERNAME, - BACKUPS_USERNAME, -] TLS_RELATION = "certificates" TLS_SSL_CA_FILE = "custom-ca.pem" TLS_SSL_KEY_FILE = "custom-server-key.pem" TLS_SSL_CERT_FILE = "custom-server-cert.pem" MYSQL_EXPORTER_PORT = "9104" CHARMED_MYSQL_SNAP_NAME = "charmed-mysql" -CHARMED_MYSQL_SNAP_REVISION = 51 +CHARMED_MYSQL_SNAP_REVISION = 66 # MySQL v8.0.33 CHARMED_MYSQLD_EXPORTER_SERVICE = "mysqld-exporter" CHARMED_MYSQLD_SERVICE = "mysqld" CHARMED_MYSQL = "charmed-mysql.mysql" @@ -51,3 +44,6 @@ XTRABACKUP_PLUGIN_DIR = "/snap/charmed-mysql/current/usr/lib/xtrabackup/plugin" ROOT_SYSTEM_USER = "root" GR_MAX_MEMBERS = 9 +HOSTNAME_DETAILS = "hostname-details" +SECRET_ID_KEY = "secret-id" +COS_AGENT_RELATION_NAME = "cos-agent" diff --git a/src/hostname_resolution.py b/src/hostname_resolution.py new file mode 100644 index 000000000..1550fb412 --- /dev/null +++ b/src/hostname_resolution.py @@ -0,0 +1,155 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Library containing logic pertaining to hostname resolutions in the VM charm.""" + +import io +import json +import logging +import socket + +from ops.charm import CharmBase, RelationDepartedEvent +from ops.framework import Object +from ops.model import BlockedStatus, Unit + +from constants import HOSTNAME_DETAILS, PEER +from ip_address_observer import IPAddressChangeCharmEvents, IPAddressObserver +from mysql_vm_helpers import MySQLFlushHostCacheError + +logger = logging.getLogger(__name__) + + +class MySQLMachineHostnameResolution(Object): + """Encapsulation of the the machine hostname resolution.""" + + on = IPAddressChangeCharmEvents() + + def __init__(self, charm: CharmBase): + super().__init__(charm, "hostname-resolution") + + self.charm = charm + + self.ip_address_observer = IPAddressObserver(charm) + + self.framework.observe(self.charm.on.config_changed, self._update_host_details_in_databag) + self.framework.observe(self.on.ip_address_change, self._update_host_details_in_databag) + + self.framework.observe( + self.charm.on[PEER].relation_changed, self._potentially_update_etc_hosts + ) + self.framework.observe( + self.charm.on[PEER].relation_departed, self._remove_host_from_etc_hosts + ) + + self.ip_address_observer.start_observer() + + def _update_host_details_in_databag(self, _) -> None: + hostname = socket.gethostname() + fqdn = socket.getfqdn() + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0) + try: + s.connect(("10.10.10.10", 1)) + ip = s.getsockname()[0] + except Exception: + logger.exception("Unable to get local IP address") + ip = "127.0.0.1" + + host_details = { + "hostname": hostname, + "fqdn": fqdn, + "ip": ip, + } + + self.charm.unit_peer_data[HOSTNAME_DETAILS] = json.dumps(host_details) + + def _get_host_details(self) -> dict[str, str]: + host_details = {} + + for key, data in self.charm.peers.data.items(): + if isinstance(key, Unit) and data.get(HOSTNAME_DETAILS): + unit_details = json.loads(data[HOSTNAME_DETAILS]) + unit_details["unit"] = key.name + host_details[unit_details["hostname"]] = unit_details + + return host_details + + def _does_etc_hosts_need_update(self, host_details: dict[str, str]) -> bool: + outdated_hosts = host_details.copy() + + with open("/etc/hosts", "r") as hosts_file: + for line in hosts_file: + if "# unit=" not in line: + continue + + ip, fqdn, hostname = line.split("#")[0].strip().split() + if outdated_hosts.get(hostname).get("ip") == ip: + outdated_hosts.pop(hostname) + + return bool(outdated_hosts) + + def _potentially_update_etc_hosts(self, _) -> None: + """Potentially update the /etc/hosts file with new hostname to IP for units.""" + if not self.charm._is_peer_data_set: + return + + host_details = self._get_host_details() + if not host_details: + logger.debug("No hostnames in the peer databag. Skipping update of /etc/hosts") + + if not self._does_etc_hosts_need_update(host_details): + logger.debug("No hostnames in /etc/hosts changed. Skipping update to /etc/hosts") + + hosts_in_file = [] + + with io.StringIO() as updated_hosts_file: + with open("/etc/hosts", "r") as hosts_file: + for line in hosts_file: + if "# unit=" not in line: + updated_hosts_file.write(line) + continue + + for hostname, details in host_details.items(): + if hostname == line.split()[2]: + hosts_in_file.append(hostname) + + fqdn, ip, unit = details["fqdn"], details["ip"], details["unit"] + + logger.info( + f"Overwriting {hostname} ({unit=}) with {ip=}, {fqdn=} in /etc/hosts" + ) + updated_hosts_file.write(f"{ip} {fqdn} {hostname} # unit={unit}\n") + break + + for hostname, details in host_details.items(): + if hostname not in hosts_in_file: + fqdn, ip, unit = details["fqdn"], details["ip"], details["unit"] + + logger.info(f"Adding {hostname} ({unit=} with {ip=}, {fqdn=} in /etc/hosts") + updated_hosts_file.write(f"{ip} {fqdn} {hostname} # unit={unit}\n") + + with open("/etc/hosts", "w") as hosts_file: + hosts_file.write(updated_hosts_file.getvalue()) + + try: + self.charm._mysql.flush_host_cache() + except MySQLFlushHostCacheError: + self.charm.unit.status = BlockedStatus("Unable to flush MySQL host cache") + + def _remove_host_from_etc_hosts(self, event: RelationDepartedEvent) -> None: + departing_unit_name = event.unit.name + + with io.StringIO() as updated_hosts_file: + with open("/etc/hosts", "r") as hosts_file: + for line in hosts_file: + if f"# unit={departing_unit_name}" not in line: + updated_hosts_file.write(line) + + with open("/etc/hosts", "w") as hosts_file: + hosts_file.write(updated_hosts_file.getvalue()) + + try: + self.charm._mysql.flush_host_cache() + except MySQLFlushHostCacheError: + self.charm.unit.status = BlockedStatus("Unable to flush MySQL host cache") diff --git a/src/ip_address_observer.py b/src/ip_address_observer.py new file mode 100644 index 000000000..989a46db8 --- /dev/null +++ b/src/ip_address_observer.py @@ -0,0 +1,141 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""IP address changes observer.""" + +import logging +import os +import signal +import socket +import subprocess +import sys +import time + +from ops.charm import CharmBase, CharmEvents +from ops.framework import EventBase, EventSource, Object +from ops.model import ActiveStatus + +logger = logging.getLogger(__name__) + +# File path for the spawned ip address observer process to write logs. +LOG_FILE_PATH = "/var/log/ip_address_observer.log" + + +class IPAddressChangeEvent(EventBase): + """A custom event for IP address change.""" + + +class IPAddressChangeCharmEvents(CharmEvents): + """A CharmEvents extension for IP address changes. + + Includes :class:`IPAddressChangeEvent` in those that can be handled. + """ + + ip_address_change = EventSource(IPAddressChangeEvent) + + +class IPAddressObserver(Object): + """Observes changes in the unit's IP address. + + Observed IP address changes cause :class:`IPAddressChangeEvent` to be emitted. + """ + + def __init__(self, charm: CharmBase): + super().__init__(charm, "ip-address-observer") + + self.charm = charm + + def start_observer(self): + """Start the IP address observer running in a new process.""" + if ( + not isinstance(self.charm.unit.status, ActiveStatus) + or self.charm.peers is None + or "observer-pid" in self.charm.unit_peer_data + ): + return + + logger.info("Starting IP address observer process") + + # We need to trick Juju into thinking that we are not running + # in a hook context, as Juju will disallow use of juju-run. + new_env = os.environ.copy() + if "JUJU_CONTEXT_ID" in new_env: + new_env.pop("JUJU_CONTEXT_ID") + + pid = subprocess.Popen( + [ + "/usr/bin/python3", + "src/ip_address_observer.py", + "/usr/bin/juju-run", + self.charm.unit.name, + self.charm.charm_dir, + ], + stdout=open(LOG_FILE_PATH, "a"), + stderr=subprocess.STDOUT, + env=new_env, + ) + + self.charm.unit_peer_data.update({"observer-pid": f"{pid}"}) + logging.info(f"Started IP address observer process with PID {pid}") + + def stop_observer(self): + """Stop running the observer if it is indeed running.""" + if self.charm.peers is None or "observer-pid" not in self.charm.unit_peer_data: + return + + observer_pid = int(self.charm.unit_peer_data["observer-pid"]) + + try: + os.kill(observer_pid, signal.SIGTERM) + logger.info(f"Stopped running IP address observer process with PID {observer_pid}") + del self.charm.unit_peer_data["observer-pid"] + except OSError: + pass + + +def dispatch(run_command, unit, charm_directory): + """Use the juju-run command to dispatch :class:`IPAddressChangeEvent`.""" + dispatch_sub_command = "JUJU_DISPATCH_PATH=hooks/ip_address_change {}/dispatch" + subprocess.run([run_command, "-u", unit, dispatch_sub_command.format(charm_directory)]) + + +def main(): + """Main watch and dispatch loop. + + Determine the host IP address every 30 seconds, and dispatch and event if it + changes. + """ + run_command, unit, charm_directory = sys.argv[1:] + + def _get_local_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0) + + try: + s.connect(("10.10.10.10", 1)) + ip = s.getsockname()[0] + except Exception: + logger.exception("Unable to get local IP address") + ip = "127.0.0.1" + + return ip + + previous_ip_address = None + while True: + ip_address = _get_local_ip() + + if not previous_ip_address: + print(f"Setting initial ip address to {ip_address}") + sys.stdout.flush() + previous_ip_address = ip_address + elif ip_address != previous_ip_address: + print(f"Detected ip address change from {previous_ip_address} to {ip_address}") + sys.stdout.flush() + previous_ip_address = ip_address + dispatch(run_command, unit, charm_directory) + + time.sleep(30) + + +if __name__ == "__main__": + main() diff --git a/src/mysql_vm_helpers.py b/src/mysql_vm_helpers.py index 5a2d484d9..16c37755f 100644 --- a/src/mysql_vm_helpers.py +++ b/src/mysql_vm_helpers.py @@ -7,11 +7,13 @@ import os import pathlib import shutil +import socket import subprocess import tempfile from typing import Dict, List, Optional, Tuple from charms.mysql.v0.mysql import ( + BYTES_1MiB, Error, MySQLBase, MySQLClientError, @@ -22,7 +24,6 @@ MySQLStartMySQLDError, MySQLStopMySQLDError, ) -from charms.operator_libs_linux.v0 import apt from charms.operator_libs_linux.v1 import snap from tenacity import retry, stop_after_delay, wait_fixed @@ -73,6 +74,10 @@ class MySQLExporterConnectError(Error): """Exception raised when there's an error setting up MySQL exporter.""" +class MySQLFlushHostCacheError(Error): + """Exception raised when there's an error flushing the MySQL host cache.""" + + class MySQL(MySQLBase): """Class to encapsulate all operations related to the MySQL instance and cluster. @@ -131,19 +136,30 @@ def install_and_configure_mysql_dependencies() -> None: """Install and configure MySQL dependencies. Raises - subprocess.CalledProcessError: if issue updating apt or creating mysqlsh common dir - apt.PackageNotFoundError, apt.PackageError: if issue install mysql server - snap.SnapNotFOundError, snap.SnapError: if issue installing mysql shell snap + subprocess.CalledProcessError: if issue creating mysqlsh common dir + snap.SnapNotFoundError, snap.SnapError: if issue installing charmed-mysql snap """ + logger.debug("Retrieving snap cache") + cache = snap.SnapCache() + charmed_mysql = cache[CHARMED_MYSQL_SNAP_NAME] + # This charm can override/use an existing snap installation only if the snap was previously + # installed by this charm. + # Otherwise, the snap could be in use by another charm (e.g. MySQL Router charm). + installed_by_mysql_server_file = pathlib.Path( + CHARMED_MYSQL_COMMON_DIRECTORY, "installed_by_mysql_server_charm" + ) + if charmed_mysql.present and not installed_by_mysql_server_file.exists(): + logger.error( + f"{CHARMED_MYSQL_SNAP_NAME} snap already installed on machine. Installation aborted" + ) + raise Exception( + f"Multiple {CHARMED_MYSQL_SNAP_NAME} snap installs not supported on one machine" + ) + try: # install the charmed-mysql snap - logger.debug("Retrieving snap cache") - cache = snap.SnapCache() - charmed_mysql = cache[CHARMED_MYSQL_SNAP_NAME] - - if not charmed_mysql.present: - logger.debug("Installing charmed-mysql snap") - charmed_mysql.ensure(snap.SnapState.Present, revision=CHARMED_MYSQL_SNAP_REVISION) + logger.debug("Installing charmed-mysql snap") + charmed_mysql.ensure(snap.SnapState.Present, revision=CHARMED_MYSQL_SNAP_REVISION) # ensure creation of mysql shell common directory by running 'mysqlsh --help' if not os.path.exists(CHARMED_MYSQL_COMMON_DIRECTORY): @@ -151,35 +167,43 @@ def install_and_configure_mysql_dependencies() -> None: mysqlsh_help_command = ["charmed-mysql.mysqlsh", "--help"] subprocess.check_call(mysqlsh_help_command, stderr=subprocess.PIPE) - except subprocess.CalledProcessError as e: - logger.exception("Failed to execute subprocess command", exc_info=e) - raise - except (apt.PackageNotFoundError, apt.PackageError) as e: - logger.exception("Failed to install apt packages", exc_info=e) + subprocess.run(["snap", "alias", "charmed-mysql.mysql", "mysql"], check=True) + + installed_by_mysql_server_file.touch(exist_ok=True) + except subprocess.CalledProcessError: + logger.exception("Failed to execute subprocess command") raise - except (snap.SnapNotFoundError, snap.SnapError) as e: - logger.exception("Failed to install snaps", exc_info=e) + except (snap.SnapNotFoundError, snap.SnapError): + logger.exception("Failed to install snaps") raise - except Exception as e: - logger.exception("Encountered an unexpected exception", exc_info=e) + except Exception: + logger.exception("Encountered an unexpected exception") raise - def create_custom_mysqld_config(self) -> None: + def create_custom_mysqld_config(self, profile: str) -> None: """Create custom mysql config file. Raises MySQLCreateCustomMySQLDConfigError if there is an error creating the custom mysqld config """ - try: - ( - innodb_buffer_pool_size, - innodb_buffer_pool_chunk_size, - ) = self.get_innodb_buffer_pool_parameters() - max_connections = self.get_max_connections() - except MySQLGetAutoTunningParametersError: - raise MySQLCreateCustomMySQLDConfigError( - "Failed to compute mysql parameters automatically" - ) + group_replication_message_cache_size = None + if profile == "testing": + innodb_buffer_pool_size = 20 * BYTES_1MiB + innodb_buffer_pool_chunk_size = 1 * BYTES_1MiB + group_replication_message_cache_size = 128 * BYTES_1MiB + max_connections = 20 + else: + try: + ( + innodb_buffer_pool_size, + innodb_buffer_pool_chunk_size, + group_replication_message_cache_size, + ) = self.get_innodb_buffer_pool_parameters() + max_connections = self.get_max_connections() + except MySQLGetAutoTunningParametersError: + raise MySQLCreateCustomMySQLDConfigError( + "Failed to compute mysql parameters automatically" + ) content = [ "[mysqld]", @@ -192,7 +216,12 @@ def create_custom_mysqld_config(self) -> None: if innodb_buffer_pool_chunk_size: content.append(f"innodb_buffer_pool_chunk_size = {innodb_buffer_pool_chunk_size}") - content.append(f"report_host = {self.instance_address}") + if group_replication_message_cache_size: + content.append( + f"loose-group_replication_message_cache_size = {group_replication_message_cache_size}" + ) + + content.append(f"report_host = {socket.getfqdn()}") content.append("") # create the mysqld config directory if it does not exist @@ -477,7 +506,7 @@ def start_mysqld(self) -> None: ) try: - snap_service_operation(CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start", True) + snap_service_operation(CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start") self.wait_until_mysql_connection() except ( MySQLServiceNotRunningError, @@ -488,6 +517,21 @@ def start_mysqld(self) -> None: raise MySQLStartMySQLDError(e.message) + def flush_host_cache(self) -> None: + """Flush the MySQL in-memory host cache.""" + flush_host_cache_command = "TRUNCATE TABLE performance_schema.host_cache" + + try: + logger.info("Truncating the MySQL host cache") + self._run_mysqlcli_script( + flush_host_cache_command, + user=self.server_config_user, + password=self.server_config_password, + ) + except MySQLClientError as e: + logger.exception("Failed to truncate the MySQL host cache") + raise MySQLFlushHostCacheError(e.message) + def connect_mysql_exporter(self) -> None: """Set up mysqld-exporter config options. @@ -506,11 +550,21 @@ def connect_mysql_exporter(self) -> None: } ) snap_service_operation( - CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_EXPORTER_SERVICE, "start", True + CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_EXPORTER_SERVICE, "start" ) except snap.SnapError: logger.exception("An exception occurred when setting up mysqld-exporter.") - raise MySQLExporterConnectError + raise MySQLExporterConnectError("Error setting up mysqld-exporter") + + def stop_mysql_exporter(self) -> None: + """Stop the mysqld exporter.""" + try: + snap_service_operation( + CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_EXPORTER_SERVICE, "stop" + ) + except snap.SnapError: + logger.exception("An exception occurred when stopping mysqld-exporter") + raise MySQLExporterConnectError("Error stopping mysqld-exporter") def _run_mysqlsh_script(self, script: str, timeout=None) -> str: """Execute a MySQL shell script. @@ -528,11 +582,13 @@ def _run_mysqlsh_script(self, script: str, timeout=None) -> str: _file.write(script) _file.flush() - # Specify python as this is not the default in the deb version - # of the mysql-shell snap command = [CHARMED_MYSQLSH, "--no-wizard", "--python", "-f", _file.name] try: + # need to change permissions since charmed-mysql.mysqlsh runs as + # snap_daemon + shutil.chown(_file.name, user="snap_daemon", group="root") + return subprocess.check_output( command, stderr=subprocess.PIPE, timeout=timeout ).decode("utf-8") @@ -693,7 +749,7 @@ def instance_hostname(): return None -def snap_service_operation(snapname: str, service: str, operation: str, enable=False) -> bool: +def snap_service_operation(snapname: str, service: str, operation: str) -> bool: """Helper function to run an operation on a snap service. Args: @@ -719,10 +775,10 @@ def snap_service_operation(snapname: str, service: str, operation: str, enable=F selected_snap.restart(services=[service]) return selected_snap.services[service]["active"] elif operation == "start": - selected_snap.start(services=[service], enable=enable) + selected_snap.start(services=[service], enable=True) return selected_snap.services[service]["active"] else: - selected_snap.stop(services=[service]) + selected_snap.stop(services=[service], disable=True) return not selected_snap.services[service]["active"] except snap.SnapError: error_message = f"Failed to run snap service operation, snap={snapname}, service={service}, operation={operation}" diff --git a/src/relations/mysql.py b/src/relations/mysql.py index fd9d9dd61..a875c878b 100644 --- a/src/relations/mysql.py +++ b/src/relations/mysql.py @@ -13,13 +13,17 @@ ) from ops.charm import RelationBrokenEvent, RelationCreatedEvent from ops.framework import Object -from ops.model import BlockedStatus +from ops.model import ActiveStatus, BlockedStatus -from constants import LEGACY_MYSQL, PASSWORD_LENGTH +from constants import LEGACY_MYSQL, PASSWORD_LENGTH, ROOT_PASSWORD_KEY from utils import generate_random_password logger = logging.getLogger(__name__) +MYSQL_RELATION_DATA_KEY = "mysql_relation_data" +MYSQL_RELATION_USER_KEY = "mysql-interface-user" +MYSQL_RELATION_DATABASE_KEY = "mysql-interface-database" + class MySQLRelation(Object): """Encapsulation of the legacy mysql relation.""" @@ -30,6 +34,7 @@ def __init__(self, charm): self.charm = charm self.framework.observe(self.charm.on.leader_elected, self._on_leader_elected) + self.framework.observe(self.charm.on.config_changed, self._on_config_changed) self.framework.observe( self.charm.on[LEGACY_MYSQL].relation_created, self._on_mysql_relation_created ) @@ -38,8 +43,8 @@ def __init__(self, charm): self.charm.on[LEGACY_MYSQL].relation_broken, self._on_mysql_relation_broken ) - def _get_or_set_password_in_peer_databag(self, username: str) -> str: - """Get a user's password from the peer databag if it exists, else populate a password. + def _get_or_set_password_in_peer_secrets(self, username: str) -> str: + """Get a user's password from the peer secrets, if it exists, else populate a password. Args: username: The mysql username @@ -47,16 +52,36 @@ def _get_or_set_password_in_peer_databag(self, username: str) -> str: Returns: a string representing the password for the mysql user """ - peer_databag = self.charm.app_peer_data - - if peer_databag.get(f"{username}_password"): - return peer_databag.get(f"{username}_password") + password_key = f"{username}-password" + fallback_key = f"{username}_password" + password = self.charm.get_secret("app", password_key, fallback_key=fallback_key) + if password: + return password password = generate_random_password(PASSWORD_LENGTH) - peer_databag[f"{username}_password"] = password - + self.charm.set_secret("app", password_key, password, fallback_key=fallback_key) return password + def _get_or_generate_username(self, event_relation_id: int) -> str: + """Retrieve username from databag or config or generate a new one. + + Assumes that the caller is the leader unit. + """ + return self.charm.app_peer_data.setdefault( + MYSQL_RELATION_USER_KEY, + self.charm.config.get(MYSQL_RELATION_USER_KEY) or f"relation-{event_relation_id}", + ) + + def _get_or_generate_database(self, event_relation_id: int) -> str: + """Retrieve database from databag or config or generate a new one. + + Assumes that the caller is the leader unit. + """ + return self.charm.app_peer_data.setdefault( + MYSQL_RELATION_DATABASE_KEY, + self.charm.config.get(MYSQL_RELATION_DATABASE_KEY) or f"database-{event_relation_id}", + ) + def _on_leader_elected(self, _) -> None: """Handle the leader elected event. @@ -67,7 +92,7 @@ def _on_leader_elected(self, _) -> None: if not self.charm._is_peer_data_set: return - relation_data = json.loads(self.charm.app_peer_data.get("mysql_relation_data", "{}")) + relation_data = json.loads(self.charm.app_peer_data.get(MYSQL_RELATION_DATA_KEY, "{}")) for relation in self.charm.model.relations.get(LEGACY_MYSQL, []): relation_databag = relation.data @@ -81,8 +106,30 @@ def _on_leader_elected(self, _) -> None: primary_address = self.charm._mysql.get_cluster_primary_address().split(":")[0] relation_databag[self.charm.unit]["host"] = primary_address + def _on_config_changed(self, _) -> None: + """Handle the change of the username/database in config.""" + if not self.charm.unit.is_leader(): + return + + if not ( + self.charm.app_peer_data.get(MYSQL_RELATION_USER_KEY) + and self.charm.app_peer_data.get(MYSQL_RELATION_DATABASE_KEY) + ): + return + + if isinstance(self.charm.unit.status, ActiveStatus) and self.model.relations.get( + LEGACY_MYSQL + ): + for key in (MYSQL_RELATION_USER_KEY, MYSQL_RELATION_DATABASE_KEY): + config_value = self.charm.config.get(key) + if config_value and config_value != self.charm.app_peer_data[key]: + self.charm.app.status = BlockedStatus( + f"Remove `mysql` relations in order to change `{key}` config" + ) + return + def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None: - """Handle the legacy 'mysql' relation created event. + """Handle the legacy `mysql` relation created event. Will set up the database and the scoped application user. The connection data (relation data) is then copied into the peer relation databag (to @@ -100,27 +147,20 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None: logger.warning("DEPRECATION WARNING - `mysql` is a legacy interface") - # username and database are defined in application config - # if not defined, defaults applies - username = self.charm.config.get("mysql-interface-user") - database = self.charm.config.get("mysql-interface-database") - - if not username or not database: - logger.debug("`mysql` legacy interface user or database not defined.") - event.defer() - return - # wait until the unit is initialized if not self.charm.unit_peer_data.get("unit-initialized"): event.defer() return + username = self._get_or_generate_username(event.relation.id) + database = self._get_or_generate_database(event.relation.id) + # Only execute if the application user does not exist # since it could have been created by another related app if self.charm._mysql.does_mysql_user_exist(username, "%"): return - password = self._get_or_set_password_in_peer_databag(username) + password = self._get_or_set_password_in_peer_secrets(username) try: self.charm._mysql.create_application_database_and_scoped_user( @@ -137,7 +177,7 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None: MySQLCreateApplicationDatabaseAndScopedUserError, MySQLGetClusterPrimaryAddressError, ): - self.charm.unit.status = BlockedStatus("Failed to initialize mysql relation") + self.charm.unit.status = BlockedStatus("Failed to initialize `mysql` relation") return updates = { @@ -145,17 +185,20 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None: "host": primary_address, "password": password, "port": "3306", - "root_password": self.charm.app_peer_data["root-password"], + "root_password": self.charm.get_secret("app", ROOT_PASSWORD_KEY), "user": username, } event.relation.data[self.charm.unit].update(updates) + self.charm.app_peer_data[MYSQL_RELATION_USER_KEY] = username + self.charm.app_peer_data[MYSQL_RELATION_DATABASE_KEY] = database + # Store the relation data into the peer relation databag self.charm.app_peer_data["mysql_relation_data"] = json.dumps(updates) def _on_mysql_relation_broken(self, event: RelationBrokenEvent) -> None: - """Handle the 'mysql' legacy relation broken event. + """Handle the `mysql` legacy relation broken event. Delete the application user created in the relation created event handler. @@ -174,3 +217,14 @@ def _on_mysql_relation_broken(self, event: RelationBrokenEvent) -> None: except MySQLDeleteUsersForUnitError: logger.error("Failed to delete mysql users") self.charm.unit.status = BlockedStatus("Failed to remove relation user") + return + + del self.charm.app_peer_data[MYSQL_RELATION_USER_KEY] + del self.charm.app_peer_data[MYSQL_RELATION_DATABASE_KEY] + + if isinstance( + self.charm.app.status, BlockedStatus + ) and self.charm.app.status.message.startswith( + "Remove `mysql` relations in order to change" + ): + self.charm.app.status = ActiveStatus() diff --git a/src/relations/shared_db.py b/src/relations/shared_db.py index 9cf1269fc..e1d68a505 100644 --- a/src/relations/shared_db.py +++ b/src/relations/shared_db.py @@ -165,9 +165,11 @@ def _on_shared_db_relation_changed(self, event: RelationChangedEvent) -> None: # Database port is static in legacy charm local_app_data["db_port"] = local_unit_data["db_port"] = "3306" + # Wait timeout is a config option in legacy charm - # defaulted to 3600 seconds - local_app_data["wait_timeout"] = local_unit_data["wait_timeout"] = "3600" + # here defaulted to mysql sysvar value: + # https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_wait_timeout + local_app_data["wait_timeout"] = local_unit_data["wait_timeout"] = "28800" # password already cached local_app_data["password"] = local_unit_data["password"] = password diff --git a/tests/conftest.py b/tests/conftest.py index 926a090d2..7e438b377 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,10 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -from argparse import ArgumentError + +import argparse def pytest_addoption(parser): - parser.addoption( - "--collect-groups", - action="store_true", - help="Collect test groups (used by GitHub Actions)", - ) - parser.addoption("--group", type=int, help="Integration test group number") parser.addoption("--mysql-charm-series", help="Ubuntu series for mysql charm (e.g. jammy)") parser.addoption( "--mysql-charm-bases-index", @@ -19,12 +14,10 @@ def pytest_addoption(parser): def pytest_configure(config): - if config.option.collect_groups: - config.option.collectonly = True if (config.option.mysql_charm_series is None) ^ ( config.option.mysql_charm_bases_index is None ): - raise ArgumentError( + raise argparse.ArgumentError( None, "--mysql-charm-series and --mysql-charm-bases-index must be given together" ) # Note: Update defaults whenever charmcraft.yaml is changed diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 445296ef0..f8951d616 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,80 +1,10 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import dataclasses -import json -import os -from pathlib import Path -from typing import Optional -import pytest -from pytest_operator.plugin import OpsTest - - -def _get_group_number(function) -> Optional[int]: - """Gets group number from test function marker. - - This example has a group number of 1: - @pytest.mark.group(1) - def test_build_and_deploy(): - pass - """ - group_markers = [marker for marker in function.own_markers if marker.name == "group"] - if not group_markers: - return - assert len(group_markers) == 1 - marker_args = group_markers[0].args - assert len(marker_args) == 1 - group_number = marker_args[0] - assert isinstance(group_number, int) - return group_number - - -def _collect_groups(items): - """Collects unique group numbers for each test module.""" +import pathlib - @dataclasses.dataclass(eq=True, order=True, frozen=True) - class Group: - path_to_test_file: str - group_number: int - job_name: str - - groups: set[Group] = set() - for function in items: - if not (group_number := _get_group_number(function)): - continue - # Example: "integration.relations.test_database" - name = function.module.__name__ - assert name.split(".")[0] == "integration" - # Example: "tests/integration/relations/test_database.py" - path_to_test_file = f"tests/{name.replace('.', '/')}.py" - # Example: "relations/test_database.py | group 1" - job_name = f"{'/'.join(path_to_test_file.split('/')[2:])} | group {group_number}" - groups.add(Group(path_to_test_file, group_number, job_name)) - sorted_groups: list[dict] = [dataclasses.asdict(group) for group in sorted(list(groups))] - output = f"groups={json.dumps(sorted_groups)}" - print(f"\n\n{output}\n") - output_file = os.environ["GITHUB_OUTPUT"] - with open(output_file, "a") as file: - file.write(output) - - -def pytest_collection_modifyitems(config, items): - if config.option.collect_groups: - _collect_groups(items) - elif selected_group_number := config.option.group: - # Remove tests that do not match the selected group number - filtered_items = [] - for function in items: - group_number = _get_group_number(function) - if not group_number: - function.add_marker(pytest.mark.skip("Missing group number")) - filtered_items.append(function) - elif group_number == selected_group_number: - filtered_items.append(function) - assert ( - len({function.module.__name__ for function in filtered_items}) == 1 - ), "Only 1 test module can be run if --group is specified" - items[:] = filtered_items +import pytest +import pytest_operator.plugin @pytest.fixture(scope="session") @@ -83,24 +13,13 @@ def mysql_charm_series(pytestconfig) -> str: @pytest.fixture(scope="module") -def ops_test(ops_test: OpsTest, pytestconfig) -> OpsTest: - if os.environ.get("CI") == "true": - # Running in GitHub Actions; skip build step - # (GitHub Actions uses a separate, cached build step. See .github/workflows/ci.yaml) - packed_charms = json.loads(os.environ["CI_PACKED_CHARMS"]) - - async def _build_charm(charm_path, bases_index: int = None) -> Path: - for charm in packed_charms: - if Path(charm_path) == Path(charm["directory_path"]): - if bases_index is None or bases_index == charm["bases_index"]: - return charm["file_path"] - raise ValueError(f"Unable to find .charm file for {bases_index=} at {charm_path=}") - - else: - _build_charm = ops_test.build_charm +def ops_test( + ops_test: pytest_operator.plugin.OpsTest, pytestconfig +) -> pytest_operator.plugin.OpsTest: + _build_charm = ops_test.build_charm - async def build_charm(charm_path) -> Path: - if Path(charm_path) == Path("."): + async def build_charm(charm_path) -> pathlib.Path: + if pathlib.Path(charm_path) == pathlib.Path("."): # Building mysql charm return await _build_charm( charm_path, diff --git a/tests/integration/high_availability/high_availability_helpers.py b/tests/integration/high_availability/high_availability_helpers.py index b68a5f9a5..33126b8a0 100644 --- a/tests/integration/high_availability/high_availability_helpers.py +++ b/tests/integration/high_availability/high_availability_helpers.py @@ -137,7 +137,7 @@ async def deploy_and_scale_mysql( charm = await ops_test.build_charm(".") - config = {"cluster-name": CLUSTER_NAME} + config = {"cluster-name": CLUSTER_NAME, "profile": "testing"} async with ops_test.fast_forward(): await ops_test.model.deploy( diff --git a/tests/integration/high_availability/test_replication.py b/tests/integration/high_availability/test_replication.py index 2958610c9..d82a902cc 100644 --- a/tests/integration/high_availability/test_replication.py +++ b/tests/integration/high_availability/test_replication.py @@ -4,22 +4,24 @@ import logging +import time from pathlib import Path import pytest import urllib3 import yaml from pytest_operator.plugin import OpsTest +from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed from ..helpers import ( cluster_name, execute_queries_on_unit, + fetch_credentials, generate_random_string, get_primary_unit, get_primary_unit_wrapper, get_server_config_credentials, retrieve_database_variable_value, - run_command_on_unit, scale_application, ) from .high_availability_helpers import ( @@ -54,6 +56,50 @@ async def test_exporter_endpoints(ops_test: OpsTest, mysql_charm_series: str) -> http = urllib3.PoolManager() for unit in application.units: + _, output, _ = await ops_test.juju( + "ssh", unit.name, "sudo", "snap", "services", "charmed-mysql.mysqld-exporter" + ) + assert output.split("\n")[1].split()[2] == "inactive" + + return_code, _, _ = await ops_test.juju( + "ssh", unit.name, "sudo", "snap", "set", "charmed-mysql", "exporter.user=monitoring" + ) + assert return_code == 0 + + monitoring_credentials = await fetch_credentials(unit, "monitoring") + return_code, _, _ = await ops_test.juju( + "ssh", + unit.name, + "sudo", + "snap", + "set", + "charmed-mysql", + f"exporter.password={monitoring_credentials['password']}", + ) + assert return_code == 0 + + return_code, _, _ = await ops_test.juju( + "ssh", unit.name, "sudo", "snap", "start", "charmed-mysql.mysqld-exporter" + ) + assert return_code == 0 + + try: + for attempt in Retrying(stop=stop_after_attempt(45), wait=wait_fixed(2)): + with attempt: + _, output, _ = await ops_test.juju( + "ssh", + unit.name, + "sudo", + "snap", + "services", + "charmed-mysql.mysqld-exporter", + ) + assert output.split("\n")[1].split()[2] == "active" + except RetryError: + raise Exception("Failed to start the mysqld-exporter snap service") + + time.sleep(30) + unit_address = await unit.get_public_address() mysql_exporter_url = f"http://{unit_address}:9104/metrics" @@ -70,12 +116,8 @@ async def test_custom_variables(ops_test: OpsTest, mysql_charm_series) -> None: application = ops_test.model.applications[mysql_application_name] for unit in application.units: - unit_total_mem = await run_command_on_unit( - unit, "grep MemTotal /proc/meminfo | awk '{print $2}'" - ) - unit_total_mem = 1024 * int(unit_total_mem.strip()) custom_vars = {} - custom_vars["max_connections"] = unit_total_mem // 12582912 + custom_vars["max_connections"] = 20 for k, v in custom_vars.items(): logger.info(f"Checking that {k} is set to {v} on {unit.name}") value = await retrieve_database_variable_value(ops_test, unit, k) diff --git a/tests/integration/relations/test_database.py b/tests/integration/relations/test_database.py index c127d03b5..701a5fc90 100644 --- a/tests/integration/relations/test_database.py +++ b/tests/integration/relations/test_database.py @@ -53,7 +53,7 @@ async def test_build_and_deploy(ops_test: OpsTest, mysql_charm_series: str) -> N """Build the charm and deploy 3 units to ensure a cluster is formed.""" db_charm = await ops_test.build_charm(".") - config = {"cluster-name": CLUSTER_NAME} + config = {"cluster-name": CLUSTER_NAME, "profile": "testing"} await asyncio.gather( ops_test.model.deploy( diff --git a/tests/integration/relations/test_db_router.py b/tests/integration/relations/test_db_router.py index 843c9b36a..eb5e9ae46 100644 --- a/tests/integration/relations/test_db_router.py +++ b/tests/integration/relations/test_db_router.py @@ -120,7 +120,7 @@ async def test_keystone_bundle_db_router(ops_test: OpsTest, mysql_charm_series: """ charm = await ops_test.build_charm(".") - config = {"cluster-name": CLUSTER_NAME} + config = {"cluster-name": CLUSTER_NAME, "profile": "testing"} mysql_app = await ops_test.model.deploy( charm, application_name=APP_NAME, config=config, num_units=1, series=mysql_charm_series diff --git a/tests/integration/relations/test_relation_mysql_legacy.py b/tests/integration/relations/test_relation_mysql_legacy.py index 22f0900a9..0919e5a7c 100644 --- a/tests/integration/relations/test_relation_mysql_legacy.py +++ b/tests/integration/relations/test_relation_mysql_legacy.py @@ -41,7 +41,7 @@ async def test_build_and_deploy(ops_test: OpsTest, mysql_charm_series: str) -> N """Build the charm and deploy 3 units to ensure a cluster is formed.""" db_charm = await ops_test.build_charm(".") - config = {"cluster-name": CLUSTER_NAME} + config = {"cluster-name": CLUSTER_NAME, "profile": "testing"} await asyncio.gather( ops_test.model.deploy( diff --git a/tests/integration/relations/test_shared_db.py b/tests/integration/relations/test_shared_db.py index 42b6c050e..b391b9262 100644 --- a/tests/integration/relations/test_shared_db.py +++ b/tests/integration/relations/test_shared_db.py @@ -41,26 +41,26 @@ async def deploy_and_relate_keystone_with_mysql( number_of_units: The number of keystone units to deploy """ # Deploy keystone - # Explicitly setting the series to 'focal' as it defaults to 'xenial' - await ops_test.model.deploy( + logger.info("Deploy keystone..") + keystone_application = await ops_test.model.deploy( "keystone", - series="focal", + channel="yoga/stable", + series="jammy", application_name=keystone_application_name, num_units=number_of_units, ) - await ops_test.model.wait_for_idle( - apps=[keystone_application_name], - status="blocked", - raise_on_blocked=False, + + await ops_test.model.block_until( + lambda: {unit.workload_status for unit in keystone_application.units} == {"blocked"}, timeout=SLOW_WAIT_TIMEOUT, ) # Relate keystone to mysql + logger.info("Relate keystone and mysql") await ops_test.model.relate(f"{keystone_application_name}:shared-db", f"{APP_NAME}:shared-db") - await ops_test.model.wait_for_idle( - apps=[keystone_application_name], - status="active", - raise_on_blocked=False, # both applications are blocked initially + logger.info("Wait keystone settle after relation") + await ops_test.model.block_until( + lambda: {unit.workload_status for unit in keystone_application.units} == {"active"}, timeout=SLOW_WAIT_TIMEOUT, ) @@ -74,6 +74,7 @@ async def check_successful_keystone_migration( ops_test: The ops test framework server_config_credentials: The credentials for the server config user """ + logger.info("Checking keystone migration") show_tables_sql = [ "SHOW DATABASES", ] @@ -155,7 +156,7 @@ async def test_keystone_bundle_shared_db(ops_test: OpsTest, mysql_charm_series: """ charm = await ops_test.build_charm(".") - config = {"cluster-name": CLUSTER_NAME} + config = {"cluster-name": CLUSTER_NAME, "profile": "testing"} await ops_test.model.deploy( charm, application_name=APP_NAME, diff --git a/tests/integration/test_tls.py b/tests/integration/test_tls.py index 9116a0166..ea88f4318 100644 --- a/tests/integration/test_tls.py +++ b/tests/integration/test_tls.py @@ -152,13 +152,14 @@ async def test_rotate_tls_key(ops_test: OpsTest) -> None: original_tls[unit.name]["cert"] = await unit_file_md5( ops_test, unit.name, - f"/var/snap/charmed-mysql/common/var/lib/mysql/data/{TLS_SSL_CERT_FILE}", + f"/var/snap/charmed-mysql/common/var/lib/mysql/{TLS_SSL_CERT_FILE}", ) # set key using auto-generated key for each unit # not asserting actions run due false positives on CI for unit in ops_test.model.applications[app].units: - await unit.run_action(action_name="set-tls-private-key") + action = await unit.run_action(action_name="set-tls-private-key") + action.wait() # Wait for hooks start reconfiguring app # add as a wait since app state does not change diff --git a/tests/unit/test_backups.py b/tests/unit/test_backups.py index 3a1ee84c4..68d393107 100644 --- a/tests/unit/test_backups.py +++ b/tests/unit/test_backups.py @@ -151,7 +151,7 @@ def test_on_list_backups_failure(self, _list_backups_in_s3_path, _retrieve_s3_pa @patch( "charms.mysql.v0.backups.MySQLBackups._can_unit_perform_backup", return_value=(True, None) ) - @patch("ops.jujuversion.JujuVersion.from_environ", return_value="test-juju-version") + @patch("ops.jujuversion.JujuVersion.from_environ", return_value=MagicMock()) @patch("charms.mysql.v0.backups.upload_content_to_s3") @patch("charms.mysql.v0.backups.MySQLBackups._pre_backup", return_value=(True, None)) @patch("charms.mysql.v0.backups.MySQLBackups._backup", return_value=(True, None)) @@ -170,6 +170,8 @@ def test_on_create_backup( _datetime, ): """Test _on_create_backup().""" + _from_environ.return_value.__str__.return_value = "test-juju-version" + _datetime.now.return_value.strftime.return_value = "2023-03-07%13:43:15Z" expected_metadata = f"""Date Backup Requested: 2023-03-07%13:43:15Z @@ -187,7 +189,7 @@ def test_on_create_backup( _retrieve_s3_parameters.assert_called_once() _can_unit_perform_backup.assert_called_once() - _from_environ.assert_called_once() + _from_environ.assert_called() _upload_content_to_s3.assert_called_once_with( expected_metadata, f"{expected_backup_path}.metadata", expected_s3_params ) @@ -207,7 +209,7 @@ def test_on_create_backup( @patch( "charms.mysql.v0.backups.MySQLBackups._can_unit_perform_backup", return_value=(True, None) ) - @patch("ops.jujuversion.JujuVersion.from_environ", return_value="test-juju-version") + @patch("ops.jujuversion.JujuVersion.from_environ", return_value=MagicMock()) @patch("charms.mysql.v0.backups.upload_content_to_s3") @patch("charms.mysql.v0.backups.MySQLBackups._pre_backup", return_value=(True, None)) @patch("charms.mysql.v0.backups.MySQLBackups._backup", return_value=(True, None)) @@ -226,6 +228,8 @@ def test_on_create_backup_failure( _datetime, ): """Test failure of _on_create_backup().""" + _from_environ.return_value.__str__.return_value = "test-juju-version" + _datetime.now.return_value.strftime.return_value = "2023-03-07%13:43:15Z" # test failure with _post_backup @@ -327,8 +331,10 @@ def test_can_unit_perform_backup( @patch_network_get(private_address="1.1.1.1") @patch("mysql_vm_helpers.MySQL.offline_mode_and_hidden_instance_exists", return_value=False) @patch("mysql_vm_helpers.MySQL.get_member_state") + @patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts") def test_can_unit_perform_backup_failure( self, + _, _get_member_state, _offline_mode_and_hidden_instance_exists, ): @@ -377,8 +383,10 @@ def test_can_unit_perform_backup_failure( @patch_network_get(private_address="1.1.1.1") @patch("mysql_vm_helpers.MySQL.set_instance_option") @patch("mysql_vm_helpers.MySQL.set_instance_offline_mode") + @patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts") def test_pre_backup( self, + _, _set_instance_offline_mode, _set_instance_option, ): @@ -545,8 +553,10 @@ def test_pre_restore_checks( @patch_network_get(private_address="1.1.1.1") @patch("mysql_vm_helpers.MySQL.is_server_connectable", return_value=True) @patch("charm.MySQLOperatorCharm.is_unit_busy", return_value=False) + @patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts") def test_pre_restore_checks_failure( self, + _, _is_unit_busy, _is_server_connectable, ): diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 8dd08632f..b550451bc 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -281,26 +281,24 @@ def test_set_secret(self, _): ) @patch_network_get(private_address="1.1.1.1") - @patch("mysql_vm_helpers.MySQL.are_locks_acquired", return_value=False) @patch("mysql_vm_helpers.MySQL.get_cluster_node_count", return_value=1) @patch("mysql_vm_helpers.MySQL.get_member_state") @patch("mysql_vm_helpers.MySQL.get_cluster_primary_address") - @patch("mysql_vm_helpers.MySQL.rescan_cluster") @patch("charm.is_volume_mounted", return_value=True) @patch("mysql_vm_helpers.MySQL.reboot_from_complete_outage") @patch("charm.snap_service_operation") @patch("charm.MySQLOperatorCharm._workload_reset") + @patch("hostname_resolution.MySQLMachineHostnameResolution._remove_host_from_etc_hosts") def test_on_update( self, + _, _workload_reset, _snap_service_operation, __reboot_from_complete_outage, _is_volume_mounted, - _rescan_cluster, _get_cluster_primary_address, _get_member_state, _get_cluster_node_count, - _are_locks_acquired, ): self.harness.remove_relation_unit(self.peer_relation_id, "mysql/1") self.harness.set_leader() @@ -327,14 +325,12 @@ def test_on_update( _is_volume_mounted.assert_called_once() _get_cluster_node_count.assert_called_once() _get_cluster_primary_address.assert_called_once() - _rescan_cluster.assert_called_once() self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus)) # test instance state = offline _get_member_state.reset_mock() _get_cluster_primary_address.reset_mock() - _rescan_cluster.reset_mock() _get_member_state.return_value = ("offline", "primary") self.harness.update_relation_data( @@ -351,13 +347,11 @@ def test_on_update( _snap_service_operation.assert_not_called() _workload_reset.assert_not_called() _get_cluster_primary_address.assert_called_once() - _rescan_cluster.assert_called_once() self.assertTrue(isinstance(self.harness.model.unit.status, MaintenanceStatus)) # test instance state = unreachable _get_member_state.reset_mock() _get_cluster_primary_address.reset_mock() - _rescan_cluster.reset_mock() __reboot_from_complete_outage.reset_mock() _snap_service_operation.return_value = False @@ -370,6 +364,5 @@ def test_on_update( _snap_service_operation.assert_called_once() _workload_reset.assert_called_once() _get_cluster_primary_address.assert_called_once() - _rescan_cluster.assert_called_once() self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus)) diff --git a/tests/unit/test_mysql.py b/tests/unit/test_mysql.py index 0e7dee246..73e90c521 100644 --- a/tests/unit/test_mysql.py +++ b/tests/unit/test_mysql.py @@ -902,19 +902,22 @@ def test_get_innodb_buffer_pool_parameters(self, _get_total_memory): """Test the successful execution of get_innodb_buffer_pool_parameters().""" _get_total_memory.return_value = 16484458496 - pool_size, chunk_size = self.mysql.get_innodb_buffer_pool_parameters() - self.assertEqual(12482248704, pool_size) - self.assertEqual(1560281088, chunk_size) + pool_size, chunk_size, gr_message_cache = self.mysql.get_innodb_buffer_pool_parameters() + self.assertEqual(11408506880, pool_size) + self.assertEqual(1426063360, chunk_size) + self.assertEqual(None, gr_message_cache) _get_total_memory.return_value = 3221000000 - pool_size, chunk_size = self.mysql.get_innodb_buffer_pool_parameters() - self.assertEqual(1610612736, pool_size) - self.assertEqual(201326592, chunk_size) + pool_size, chunk_size, gr_message_cache = self.mysql.get_innodb_buffer_pool_parameters() + self.assertEqual(1342177280, pool_size) + self.assertEqual(167772160, chunk_size) + self.assertEqual(None, gr_message_cache) _get_total_memory.return_value = 1073741825 - pool_size, chunk_size = self.mysql.get_innodb_buffer_pool_parameters() + pool_size, chunk_size, gr_message_cache = self.mysql.get_innodb_buffer_pool_parameters() self.assertEqual(536870912, pool_size) self.assertIsNone(chunk_size) + self.assertEqual(134217728, gr_message_cache) @patch("charms.mysql.v0.mysql.MySQLBase._get_total_memory") def test_get_innodb_buffer_pool_parameters_exception(self, _get_total_memory): @@ -1286,7 +1289,7 @@ def test_retrieve_backup_with_xbcloud_failure( @patch( "charms.mysql.v0.mysql.MySQLBase.get_innodb_buffer_pool_parameters", - return_value=(1234, 5678), + return_value=(1234, 5678, None), ) @patch("charms.mysql.v0.mysql.MySQLBase._execute_commands") def test_prepare_backup_for_restore( @@ -1321,7 +1324,7 @@ def test_prepare_backup_for_restore( @patch( "charms.mysql.v0.mysql.MySQLBase.get_innodb_buffer_pool_parameters", - return_value=(1234, 5678), + return_value=(1234, 5678, None), ) @patch("charms.mysql.v0.mysql.MySQLBase._execute_commands") def test_prepare_backup_for_restore_failure( diff --git a/tests/unit/test_mysqlsh_helpers.py b/tests/unit/test_mysqlsh_helpers.py index 25b802666..d1425eb6d 100644 --- a/tests/unit/test_mysqlsh_helpers.py +++ b/tests/unit/test_mysqlsh_helpers.py @@ -49,19 +49,22 @@ def setUp(self): @patch("tempfile.NamedTemporaryFile") @patch("subprocess.check_output") - def test_run_mysqlsh_script(self, _check_output, _): + @patch("shutil.chown") + def test_run_mysqlsh_script(self, _chown, _check_output, _): """Test a successful execution of run_mysqlsh_script.""" _check_output.return_value = b"stdout" self.mysql._run_mysqlsh_script("script") _check_output.assert_called_once() + _chown.assert_called_once() @patch("tempfile.NamedTemporaryFile") @patch("subprocess.check_output") - def test_run_mysqlsh_script_exception(self, _check_output, _): + @patch("shutil.chown") + def test_run_mysqlsh_script_exception(self, _, _check_output, __): """Test a failed execution of run_mysqlsh_script.""" - _check_output.side_effect = subprocess.CalledProcessError(cmd="", returncode=-1) + _check_output.side_effect = subprocess.CalledProcessError(cmd="", returncode=1) with self.assertRaises(MySQLClientError): self.mysql._run_mysqlsh_script("script") @@ -237,12 +240,15 @@ def test_snap_service_operation_exception(self, _snap_cache): _snap_cache.assert_not_called() - @patch("mysql_vm_helpers.MySQL.get_innodb_buffer_pool_parameters", return_value=(1234, 5678)) + @patch( + "mysql_vm_helpers.MySQL.get_innodb_buffer_pool_parameters", return_value=(1234, 5678, None) + ) @patch("mysql_vm_helpers.MySQL.get_max_connections", return_value=111) @patch("pathlib.Path") @patch("builtins.open") + @patch("socket.getfqdn", return_value="1.2.3.4") def test_create_custom_mysqld_config( - self, _open, _path, _get_innodb_buffer_pool_parameters, _get_max_connections + self, _, _open, _path, _get_innodb_buffer_pool_parameters, _get_max_connections ): """Test successful execution of create_custom_mysqld_config.""" self.maxDiff = None @@ -252,7 +258,7 @@ def test_create_custom_mysqld_config( _open_mock = unittest.mock.mock_open() _open.side_effect = _open_mock - self.mysql.create_custom_mysqld_config() + self.mysql.create_custom_mysqld_config(profile="production") config = "\n".join( ( @@ -262,7 +268,7 @@ def test_create_custom_mysqld_config( "innodb_buffer_pool_size = 1234", "max_connections = 111", "innodb_buffer_pool_chunk_size = 5678", - "report_host = 127.0.0.1", + "report_host = 1.2.3.4", "", ) ) @@ -284,6 +290,36 @@ def test_create_custom_mysqld_config( ), ) + # Test `testing` profile + _open_mock.reset_mock() + self.mysql.create_custom_mysqld_config(profile="testing") + + config = "\n".join( + ( + "[mysqld]", + "bind-address = 0.0.0.0", + "mysqlx-bind-address = 0.0.0.0", + "innodb_buffer_pool_size = 20971520", + "max_connections = 20", + "innodb_buffer_pool_chunk_size = 1048576", + "loose-group_replication_message_cache_size = 134217728", + "report_host = 1.2.3.4", + "", + ) + ) + + self.assertEqual( + sorted(_open_mock.mock_calls), + sorted( + [ + call(f"{MYSQLD_CONFIG_DIRECTORY}/z-custom-mysqld.cnf", "w"), + call().__enter__(), + call().write(config), + call().__exit__(None, None, None), + ] + ), + ) + @patch("mysql_vm_helpers.MySQL.get_innodb_buffer_pool_parameters", return_value=(1234, 5678)) @patch("pathlib.Path") @patch("builtins.open") @@ -300,7 +336,7 @@ def test_create_custom_mysqld_config_exception( _open.side_effect = _open_mock with self.assertRaises(MySQLCreateCustomMySQLDConfigError): - self.mysql.create_custom_mysqld_config() + self.mysql.create_custom_mysqld_config(profile="production") @patch("subprocess.run") def test_execute_commands(self, _run): @@ -375,7 +411,7 @@ def test_start_mysqld( self.mysql.start_mysqld() _snap_service_operation.assert_called_once_with( - CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start", True + CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start" ) _wait_until_mysql_connection.assert_called_once() @@ -397,3 +433,21 @@ def test_start_mysqld_failure( with self.assertRaises(MySQLStartMySQLDError): self.mysql.start_mysqld() + + @patch("pathlib.Path") + @patch("subprocess.check_call") + @patch("subprocess.run") + @patch("os.path.exists", return_value=True) + @patch("mysql_vm_helpers.snap.SnapCache") + def test_install_snap(self, _cache, _path_exists, _run, _check_call, _pathlib): + """Test execution of install_snap().""" + _mysql_snap = MagicMock() + _cache.return_value = {CHARMED_MYSQL_SNAP_NAME: _mysql_snap} + + _mysql_snap.present = False + _path_exists.return_value = False + + self.mysql.install_and_configure_mysql_dependencies() + + _check_call.assert_called_once_with(["charmed-mysql.mysqlsh", "--help"], stderr=-1) + _run.assert_called_once_with(["snap", "alias", "charmed-mysql.mysql", "mysql"], check=True) diff --git a/tests/unit/test_relation_mysql_legacy.py b/tests/unit/test_relation_mysql_legacy.py index 8c824a303..48192692b 100644 --- a/tests/unit/test_relation_mysql_legacy.py +++ b/tests/unit/test_relation_mysql_legacy.py @@ -25,14 +25,14 @@ def setUp(self): @patch("mysql_vm_helpers.MySQL.does_mysql_user_exist", return_value=False) @patch("mysql_vm_helpers.MySQL.get_cluster_primary_address", return_value="1.1.1.1:3306") @patch( - "relations.mysql.MySQLRelation._get_or_set_password_in_peer_databag", + "relations.mysql.MySQLRelation._get_or_set_password_in_peer_secrets", return_value="super_secure_password", ) @patch("mysql_vm_helpers.MySQL.create_application_database_and_scoped_user") def test_maria_db_relation_created( self, _create_application_database_and_scoped_user, - _get_or_set_password_in_peer_databag, + _get_or_set_password_in_peer_secrets, _get_cluster_primary_address, _does_mysql_user_exist, ): @@ -48,7 +48,7 @@ def test_maria_db_relation_created( self.maria_db_relation_id = self.harness.add_relation(LEGACY_MYSQL, "other-app") self.harness.add_relation_unit(self.maria_db_relation_id, "other-app/0") - self.assertEqual(_get_or_set_password_in_peer_databag.call_count, 1) + self.assertEqual(_get_or_set_password_in_peer_secrets.call_count, 1) _create_application_database_and_scoped_user.assert_called_once_with( "default_database", "mysql", @@ -81,14 +81,14 @@ def test_maria_db_relation_created( @patch("mysql_vm_helpers.MySQL.get_cluster_primary_address", return_value="1.1.1.1:3306") @patch("mysql_vm_helpers.MySQL.delete_users_for_unit") @patch( - "relations.mysql.MySQLRelation._get_or_set_password_in_peer_databag", + "relations.mysql.MySQLRelation._get_or_set_password_in_peer_secrets", return_value="super_secure_password", ) @patch("mysql_vm_helpers.MySQL.create_application_database_and_scoped_user") def test_maria_db_relation_departed( self, _create_application_database_and_scoped_user, - _get_or_set_password_in_peer_databag, + _get_or_set_password_in_peer_secrets, _delete_users_for_unit, _get_cluster_primary_address, _does_mysql_user_exist, diff --git a/tests/unit/test_shared_db.py b/tests/unit/test_shared_db.py index 4a8c608c5..9a7cb09bd 100644 --- a/tests/unit/test_shared_db.py +++ b/tests/unit/test_shared_db.py @@ -79,7 +79,7 @@ def test_shared_db_relation_changed( { "db_host": "1.1.1.1", "db_port": "3306", - "wait_timeout": "3600", + "wait_timeout": "28800", "password": "super_secure_password", "allowed_units": "other-app/0", }, diff --git a/tox.ini b/tox.ini index 4cdc9d95e..eb3600134 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,6 @@ [tox] no_package = True -skip_missing_interpreters = True env_list = lint, unit [vars] @@ -14,79 +13,80 @@ all_path = {[vars]src_path} {[vars]tests_path} {[vars]lib_path} [testenv] set_env = - PYTHONPATH = {tox_root}/lib:{[vars]src_path} - PYTHONBREAKPOINT=ipdb.set_trace - PY_COLORS=1 -pass_env = - PYTHONPATH - CHARM_BUILD_DIR - MODEL_SETTINGS + PYTHONPATH = {[vars]src_path}:{tox_root}/lib + PY_COLORS = 1 +allowlist_externals = + poetry + +[testenv:{build,pack-wrapper}] +# Wrap `charmcraft pack` +allowlist_externals = + {[testenv]allowlist_externals} + charmcraft + mv +commands_pre = + poetry export --only main,charm-libs --output requirements.txt +commands = + build: charmcraft pack {posargs} +commands_post = + mv requirements.txt requirements-last-build.txt [testenv:format] description = Apply coding style standards to code -deps = - black - isort +commands_pre = + poetry install --only format commands = - isort {[vars]all_path} - black {[vars]all_path} + poetry lock --no-update + poetry run isort {[vars]all_path} + poetry run black {[vars]all_path} [testenv:lint] description = Check code against coding style standards -deps = - black - flake8==5.0.4 # https://github.com/savoirfairelinux/flake8-copyright/issues/19 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell - shellcheck-py==0.9.0.2 -allowlist_externals = /bin/bash +allowlist_externals = + {[testenv]allowlist_externals} + find +commands_pre = + poetry install --only lint commands = + poetry lock --check # uncomment the following line if this charm owns a lib # codespell {[vars]lib_path} - codespell {tox_root} --skip {tox_root}/.git --skip {tox_root}/.tox \ - --skip {tox_root}/build --skip {tox_root}/lib --skip {tox_root}/venv \ - --skip {tox_root}/.mypy_cache --skip {tox_root}/icon.svg \ - --skip {tox_root}/coverage.lcov --skip {tox_root}/.idea + poetry run codespell {[vars]all_path} # pflake8 wrapper supports config from pyproject.toml - pflake8 {[vars]all_path} - isort --check-only --diff {[vars]all_path} - black --check --diff {[vars]all_path} - /bin/bash -c "find {[vars]all_path} -type f \( -name '*.sh' -o -name '*.bash' \) | xargs shellcheck --color=always" + poetry run pflake8 {[vars]all_path} + poetry run isort --check-only --diff {[vars]all_path} + poetry run black --check --diff {[vars]all_path} + find {[vars]all_path} -type f \( -name "*.sh" -o -name "*.bash" \) -exec poetry run shellcheck --color=always \{\} + [testenv:unit] description = Run unit tests -deps = - pytest - coverage[toml] - -r {tox_root}/requirements.txt +commands_pre = + poetry install --only main,charm-libs,unit commands = - coverage run --source={[vars]src_path},{[vars]lib_path} \ + poetry run coverage run --source={[vars]src_path},{[vars]lib_path} \ -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit - coverage report - coverage xml + poetry run coverage report + poetry run coverage xml [testenv:integration] description = Run integration tests +set_env = + {[testenv]set_env} + # Workaround for https://github.com/python-poetry/poetry/issues/6958 + POETRY_INSTALLER_PARALLEL = false pass_env = - {[testenv]pass_env} CI - CI_PACKED_CHARMS GITHUB_OUTPUT AWS_ACCESS_KEY AWS_SECRET_KEY GCP_ACCESS_KEY GCP_SECRET_KEY -deps = - juju==2.9.38.1 - mysql-connector-python - pytest - pytest-operator - pyyaml - -r {tox_root}/requirements.txt +allowlist_externals = + {[testenv:pack-wrapper]allowlist_externals} +commands_pre = + poetry install --only integration + {[testenv:pack-wrapper]commands_pre} commands = - pytest -v --tb native --log-cli-level=INFO -s --ignore={[vars]tests_path}/unit/ {posargs} + poetry run pytest -v --tb native --log-cli-level=INFO -s --ignore={[vars]tests_path}/unit/ {posargs} +commands_post = + {[testenv:pack-wrapper]commands_post}