From 322a9963eee75adc2916c2c529da3dc0c9a6a785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Wed, 22 Apr 2026 14:00:34 +0200 Subject: [PATCH 1/6] [K8s] Migrate to data-platform v1 libs --- .../{v0 => v1}/data_models.py | 25 +- .../data_platform_libs/{v0 => v1}/upgrade.py | 257 ++++++++++-------- 2 files changed, 166 insertions(+), 116 deletions(-) rename kubernetes/lib/charms/data_platform_libs/{v0 => v1}/data_models.py (95%) rename kubernetes/lib/charms/data_platform_libs/{v0 => v1}/upgrade.py (85%) diff --git a/kubernetes/lib/charms/data_platform_libs/v0/data_models.py b/kubernetes/lib/charms/data_platform_libs/v1/data_models.py similarity index 95% rename from kubernetes/lib/charms/data_platform_libs/v0/data_models.py rename to kubernetes/lib/charms/data_platform_libs/v1/data_models.py index 087f6f3c5..9d700dbbb 100644 --- a/kubernetes/lib/charms/data_platform_libs/v0/data_models.py +++ b/kubernetes/lib/charms/data_platform_libs/v1/data_models.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ ```python -from charms.data_platform_libs.v0.data_models import BaseConfigModel +from charms.data_platform_libs.v1.data_models import BaseConfigModel class MyConfig(BaseConfigModel): @@ -164,20 +164,27 @@ class MergedDataBag(ProviderDataBag, RequirerDataBag): LIBID = "cb2094c5b07d47e1bf346aaee0fcfcfe" # Increment this major API version when introducing breaking changes -LIBAPI = 0 +LIBAPI = 1 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 5 +LIBPATCH = 0 -PYDEPS = ["ops>=2.0.0", "pydantic>=1.10,<2"] +PYDEPS = ["ops>=2.0.0", "pydantic>=2,<3"] G = TypeVar("G") T = TypeVar("T", bound=BaseModel) AppModel = TypeVar("AppModel", bound=BaseModel) UnitModel = TypeVar("UnitModel", bound=BaseModel) -DataBagNativeTypes = (int, str, float) +DataBagNativeTypes = ( + int, + str, + float, + Optional[int], + Optional[str], + Optional[float], +) class BaseConfigModel(BaseModel): @@ -233,7 +240,7 @@ def write(relation_data: RelationDataContent, model: BaseModel): relation_data: pointer to the relation databag model: instance of pydantic model to be written """ - for key, value in model.dict(exclude_none=False).items(): + for key, value in model.model_dump(exclude_none=False).items(): if value: relation_data[key.replace("_", "-")] = ( str(value) @@ -255,10 +262,10 @@ def read(relation_data: MutableMapping[str, str], obj: Type[T]) -> T: **{ field_name: ( relation_data[parsed_key] - if field.outer_type_ in DataBagNativeTypes + if field_info.annotation in DataBagNativeTypes else json.loads(relation_data[parsed_key]) ) - for field_name, field in obj.__fields__.items() + for field_name, field_info in obj.model_fields.items() # pyright: ignore[reportGeneralTypeIssues] if (parsed_key := field_name.replace("_", "-")) in relation_data if relation_data[parsed_key] diff --git a/kubernetes/lib/charms/data_platform_libs/v0/upgrade.py b/kubernetes/lib/charms/data_platform_libs/v1/upgrade.py similarity index 85% rename from kubernetes/lib/charms/data_platform_libs/v0/upgrade.py rename to kubernetes/lib/charms/data_platform_libs/v1/upgrade.py index 4d909d644..421cff473 100644 --- a/kubernetes/lib/charms/data_platform_libs/v0/upgrade.py +++ b/kubernetes/lib/charms/data_platform_libs/v1/upgrade.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -263,7 +263,8 @@ def restart(self, event) -> None: import json import logging from abc import ABC, abstractmethod -from typing import Dict, List, Literal, Optional, Set, Tuple +from enum import IntEnum +from typing import Annotated, Dict, List, Literal, Optional, Set, Tuple, Union import poetry.core.constraints.version as poetry_version from ops.charm import ( @@ -274,23 +275,46 @@ def restart(self, event) -> None: UpgradeCharmEvent, ) from ops.framework import EventBase, EventSource, Object -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, Relation, Unit, WaitingStatus -from pydantic import BaseModel, root_validator, validator +from ops.model import ( + ActiveStatus, + Application, + BlockedStatus, + MaintenanceStatus, + Relation, + Unit, + WaitingStatus, +) +from pydantic import AfterValidator, BaseModel, model_validator # The unique Charmhub library identifier, never change it LIBID = "156258aefb79435a93d933409a8c8684" # Increment this major API version when introducing breaking changes -LIBAPI = 0 +LIBAPI = 1 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 18 +LIBPATCH = 0 -PYDEPS = ["pydantic>=1.10,<2", "poetry-core"] +PYDEPS = ["pydantic>=2,<3", "poetry-core"] logger = logging.getLogger(__name__) + +class UpgradeState(IntEnum): + """Integer enumeration of upgrade states. + + Lower values represent states further behind in the upgrade process. + """ + + RECOVERY = 0 + FAILED = 1 + IDLE = 2 + READY = 3 + UPGRADING = 4 + COMPLETED = 5 + + # --- DEPENDENCY RESOLUTION FUNCTIONS --- @@ -315,6 +339,15 @@ def verify_requirements(version: str, requirement: str) -> bool: # --- DEPENDENCY MODEL TYPES --- +def validate_constraint(value: str) -> str: + """Validates a single version constraint string.""" + poetry_version.parse_constraint(value) + return value + + +VersionConstraint = Annotated[str, AfterValidator(validate_constraint)] + + class DependencyModel(BaseModel): """Manager for a single dependency. @@ -343,40 +376,23 @@ class KafkaDependenciesModel(BaseModel): model = KafkaDependenciesModel(**deps) # loading dict in to model - print(model.dict()) # exporting back validated deps + print(model.model_dump()) # exporting back validated deps """ - dependencies: Dict[str, str] + dependencies: Dict[str, VersionConstraint] name: str - upgrade_supported: str + upgrade_supported: VersionConstraint version: str - @validator("dependencies", "upgrade_supported", each_item=True) - @classmethod - def dependencies_validator(cls, value): - """Validates version constraint.""" - if isinstance(value, dict): - deps = value.values() - else: - deps = [value] - - for dep in deps: - poetry_version.parse_constraint(dep) - - return value - - @root_validator(skip_on_failure=True) - @classmethod - def version_upgrade_supported_validator(cls, values): + @model_validator(mode="after") + def version_upgrade_supported_validator(self) -> "DependencyModel": """Validates specified `version` meets `upgrade_supported` requirement.""" - if not verify_requirements( - version=values.get("version"), requirement=values.get("upgrade_supported") - ): + if not verify_requirements(version=self.version, requirement=self.upgrade_supported): raise ValueError( - f"upgrade_supported value {values.get('upgrade_supported')} greater than version value {values.get('version')} for {values.get('name')}." + f"upgrade_supported value {self.upgrade_supported} greater than version value {self.version} for {self.name}." ) - return values + return self def can_upgrade(self, dependency: "DependencyModel") -> bool: """Compares two instances of :class:`DependencyModel` for upgradability. @@ -499,8 +515,6 @@ class UpgradeEvents(CharmEvents): class DataUpgrade(Object, ABC): """Manages `upgrade` relation operations for in-place upgrades.""" - STATES = ["recovery", "failed", "idle", "ready", "upgrading", "completed"] - on = UpgradeEvents() # pyright: ignore [reportAssignmentType] def __init__( @@ -551,12 +565,9 @@ def app_units(self) -> Set[Unit]: return set([self.charm.unit] + list(self.peer_relation.units)) @property - def state(self) -> Optional[str]: + def state(self) -> Optional[UpgradeState]: """The unit state from the upgrade peer relation.""" - if not self.peer_relation: - return None - - return self.peer_relation.data[self.charm.unit].get("state", None) + return self._get_unit_state(self.charm.unit) @property def stored_dependencies(self) -> Optional[BaseModel]: @@ -567,7 +578,7 @@ def stored_dependencies(self) -> Optional[BaseModel]: if not (deps := self.peer_relation.data[self.charm.app].get("dependencies", "")): return None - return type(self.dependency_model)(**json.loads(deps)) + return type(self.dependency_model).model_validate_json(deps) @property def upgrade_stack(self) -> Optional[List[int]]: @@ -607,7 +618,7 @@ def upgrade_stack(self, stack: List[int]) -> None: self._upgrade_stack = stack @property - def other_unit_states(self) -> list: + def other_unit_states(self) -> List[Optional[UpgradeState]]: """Current upgrade state for other units. Returns: @@ -616,51 +627,77 @@ def other_unit_states(self) -> list: if not self.peer_relation: return [] - return [ - self.peer_relation.data[unit].get("state", "") - for unit in list(self.peer_relation.units) - ] + return [self._get_unit_state(unit) for unit in self.peer_relation.units] @property - def unit_states(self) -> list: + def unit_states(self) -> List[Optional[UpgradeState]]: """Current upgrade state for all units. Returns: Unsorted list of upgrade states for all units. """ - if not self.peer_relation: - return [] - - return [self.peer_relation.data[unit].get("state", "") for unit in self.app_units] + return [self._get_unit_state(unit) for unit in self.app_units] @property - def cluster_state(self) -> Optional[str]: + def cluster_state(self) -> Optional[UpgradeState]: """Current upgrade state for cluster units. - Determined from :class:`DataUpgrade.STATE`, taking the lowest ordinal unit state. + Determined taking the lowest ordinal unit state. + + For example, if units have states: `[READY, UPGRADING, COMPLETED]`, + the overall state for the cluster is `READY`. - For example, if units in have states: `["ready", "upgrading", "completed"]`, - the overall state for the cluster is `ready`. + Returns: + UpgradeState enum of the furthest behind unit, or None if invalid. + """ + states = self.unit_states + + # Build a strictly typed list to satisfy type checker of min + valid_states: List[UpgradeState] = [] + for state in states: + if state is None: + return None + valid_states.append(state) + + return min(valid_states) + + @property + def idle(self) -> bool: + """Flag for whether the cluster is in an idle upgrade state. Returns: - String of upgrade state from the furthest behind unit. + True if all application units in idle state. Otherwise, False. """ if not self.unit_states: + return False + + return set(self.unit_states) == {UpgradeState.IDLE} + + def _get_unit_state(self, unit) -> Optional[UpgradeState]: + """Helper to safely parse the state string from relation data into an Enum.""" + if not self.peer_relation: + return None + + state_str = self.peer_relation.data[unit].get("state", "") + if not state_str: return None try: - return sorted(self.unit_states, key=self.STATES.index)[0] - except (ValueError, KeyError): + return UpgradeState[state_str.upper()] + except KeyError: return None - @property - def idle(self) -> Optional[bool]: - """Flag for whether the cluster is in an idle upgrade state. + def _set_state(self, entity: Union[Unit, Application], state: UpgradeState) -> None: + """Helper to safely write an Enum state to the relation databag. - Returns: - True if all application units in idle state. Otherwise False + Args: + entity: The Juju Unit or Application to update the data for. + state: The UpgradeState enum to serialize and save. """ - return set(self.unit_states) == {"idle"} + if not self.peer_relation: + return + + self.peer_relation.data[entity].update({"state": state.name.lower()}) @abstractmethod def pre_upgrade_check(self) -> None: @@ -704,7 +741,7 @@ def _repair_upgrade_stack(self) -> None: # if the first unit in the stack fails, the stack will be the same length as units # i.e this block not ran if ( - self.cluster_state in ["failed", "recovery"] + self.cluster_state in [UpgradeState.FAILED, UpgradeState.RECOVERY] and self.upgrade_stack and len(self.upgrade_stack) != len(self.app_units) and self.charm.unit.is_leader() @@ -735,7 +772,7 @@ def set_unit_failed(self, cause: Optional[str] = None) -> None: self._upgrade_stack = None self.charm.unit.status = BlockedStatus(cause if cause else "") - self.peer_relation.data[self.charm.unit].update({"state": "failed"}) + self._set_state(self.charm.unit, UpgradeState.FAILED) self.log_rollback_instructions() def set_unit_completed(self) -> None: @@ -749,7 +786,7 @@ def set_unit_completed(self) -> None: self._upgrade_stack = None self.charm.unit.status = MaintenanceStatus("upgrade completed") - self.peer_relation.data[self.charm.unit].update({"state": "completed"}) + self._set_state(self.charm.unit, UpgradeState.COMPLETED) # Emit upgrade_finished event to run unit's post upgrade operations. if self.substrate == "k8s": @@ -765,12 +802,12 @@ def _on_upgrade_created(self, event: RelationCreatedEvent) -> None: return # setting initial idle state needed to avoid execution on upgrade-changed events - self.peer_relation.data[self.charm.unit].update({"state": "idle"}) + self._set_state(self.charm.unit, UpgradeState.IDLE) if self.charm.unit.is_leader(): logger.debug("Persisting dependencies to upgrade relation data...") self.peer_relation.data[self.charm.app].update( - {"dependencies": json.dumps(self.dependency_model.dict())} + {"dependencies": self.dependency_model.model_dump_json()} ) def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: @@ -780,18 +817,18 @@ def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: return if not self.charm.unit.is_leader(): - event.fail(message="Action must be ran on the Juju leader.") + event.fail(message="Action must be run on the Juju leader.") return - if self.cluster_state == "failed": + if self.cluster_state == UpgradeState.FAILED: logger.info("Entering recovery state for rolling-back to previous version...") self._repair_upgrade_stack() self.charm.unit.status = BlockedStatus("ready to rollback application") - self.peer_relation.data[self.charm.unit].update({"state": "recovery"}) + self._set_state(self.charm.unit, UpgradeState.RECOVERY) return # checking if upgrade in progress - if self.cluster_state != "idle": + if self.cluster_state != UpgradeState.IDLE: event.fail("Cannot run pre-upgrade checks, cluster already upgrading.") return @@ -859,32 +896,31 @@ def _upgrade_supported_check(self) -> None: Raises: :class:`VersionError` if upgrading to existing `version` is not supported """ - keys = self.dependency_model.__fields__.keys() + stored_deps = self.stored_dependencies + if not stored_deps: + return - compatible = True incompatibilities: List[Tuple[str, str, str, str]] = [] - for key in keys: - old_dep: DependencyModel = getattr(self.stored_dependencies, key) + + for key in self.dependency_model.model_fields.keys(): + old_dep: DependencyModel = getattr(stored_deps, key) new_dep: DependencyModel = getattr(self.dependency_model, key) if not old_dep.can_upgrade(dependency=new_dep): - compatible = False incompatibilities.append( (key, old_dep.version, new_dep.version, new_dep.upgrade_supported) ) - base_message = "Versions incompatible" - base_cause = "Upgrades only supported for specific versions" - if not compatible: - for incompat in incompatibilities: - base_message += ( - f", {incompat[0]} {incompat[1]} can not be upgraded to {incompat[2]}" - ) - base_cause += f", {incompat[0]} versions satisfying requirement {incompat[3]}" + if incompatibilities: + messages = [] + causes = [] + for name, old_ver, new_ver, required_ver in incompatibilities: + messages.append(f", {name} {old_ver} can not be upgraded to {new_ver}") + causes.append(f"{name} versions satisfying requirement {required_ver}") raise VersionError( - message=base_message, - cause=base_cause, + message=f"Versions incompatible, {', '.join(messages)}", + cause=f"Upgrades only supported for specific versions, {', '.join(causes)}", ) def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: @@ -911,15 +947,15 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: top_unit = self.charm.model.get_unit(f"{self.charm.app.name}/{top_unit_id}") if ( top_unit == self.charm.unit - and self.peer_relation.data[self.charm.unit].get("state") == "recovery" + and self._get_unit_state(self.charm.unit) == UpgradeState.RECOVERY ): # While in a rollback and the Juju leader unit is the top unit in the upgrade stack, emit the event # for this unit to start the rollback. - self.peer_relation.data[self.charm.unit].update({"state": "ready"}) + self._set_state(self.charm.unit, UpgradeState.READY) self.on_upgrade_changed(event) return self.charm.unit.status = WaitingStatus("other units upgrading first...") - self.peer_relation.data[self.charm.unit].update({"state": "ready"}) + self._set_state(self.charm.unit, UpgradeState.READY) if len(self.app_units) == 1: # single unit upgrade, emit upgrade_granted event right away @@ -929,7 +965,7 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: # for k8s run version checks only on highest ordinal unit if ( self.charm.unit.name - == f"{self.charm.app.name}/{self.charm.app.planned_units() -1}" + == f"{self.charm.app.name}/{self.charm.app.planned_units() - 1}" ): try: self._upgrade_supported_check() @@ -939,7 +975,7 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: return # On K8s an unit that receives the upgrade-charm event is upgrading self.charm.unit.status = MaintenanceStatus("upgrading unit") - self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) + self._set_state(self.charm.unit, UpgradeState.UPGRADING) def on_upgrade_changed(self, event: EventBase) -> None: """Handler for `upgrade-relation-changed` events.""" @@ -947,30 +983,34 @@ def on_upgrade_changed(self, event: EventBase) -> None: return # if any other unit failed, don't continue with upgrade - if self.cluster_state == "failed": + if self.cluster_state == UpgradeState.FAILED: logger.debug("Cluster failed to upgrade, exiting...") return - if self.substrate == "vm" and self.cluster_state == "recovery": + if self.substrate == "vm" and self.cluster_state == UpgradeState.RECOVERY: # skip run while in recovery. The event will be retrigged when the cluster is ready logger.debug("Cluster in recovery, skip...") return # if all units completed, mark as complete if not self.upgrade_stack: - if self.state == "completed" and self.cluster_state in ["idle", "completed"]: + if self.state == UpgradeState.COMPLETED and self.cluster_state in [ + UpgradeState.IDLE, + UpgradeState.COMPLETED, + ]: logger.info("All units completed upgrade, setting idle upgrade state...") self.charm.unit.status = ActiveStatus() - self.peer_relation.data[self.charm.unit].update({"state": "idle"}) + + self._set_state(self.charm.unit, UpgradeState.IDLE) if self.charm.unit.is_leader(): logger.debug("Persisting new dependencies to upgrade relation data...") self.peer_relation.data[self.charm.app].update( - {"dependencies": json.dumps(self.dependency_model.dict())} + {"dependencies": self.dependency_model.model_dump_json()} ) return - if self.cluster_state == "idle": + if self.cluster_state == UpgradeState.IDLE: logger.debug("upgrade-changed event handled before pre-checks, exiting...") return @@ -978,16 +1018,19 @@ def on_upgrade_changed(self, event: EventBase) -> None: return # upgrade ongoing, set status for waiting units - if "upgrading" in self.unit_states and self.state in ["idle", "ready"]: + if UpgradeState.UPGRADING in self.unit_states and self.state in [ + UpgradeState.IDLE, + UpgradeState.READY, + ]: self.charm.unit.status = WaitingStatus("other units upgrading first...") # pop mutates the `upgrade_stack` attr top_unit_id = self.upgrade_stack.pop() top_unit = self.charm.model.get_unit(f"{self.charm.app.name}/{top_unit_id}") - top_state = self.peer_relation.data[top_unit].get("state") + top_state = self._get_unit_state(top_unit) # if top of stack is completed, leader pops it - if self.charm.unit.is_leader() and top_state == "completed": + if self.charm.unit.is_leader() and top_state == UpgradeState.COMPLETED: logger.debug(f"{top_unit} has finished upgrading, updating stack...") # writes the mutated attr back to rel data @@ -1002,15 +1045,15 @@ def on_upgrade_changed(self, event: EventBase) -> None: # if unit top of stack and all units ready (i.e stack), emit granted event if ( self.charm.unit == top_unit - and top_state in ["ready", "upgrading"] - and self.cluster_state == "ready" - and "upgrading" not in self.other_unit_states + and top_state in [UpgradeState.READY, UpgradeState.UPGRADING] + and self.cluster_state == UpgradeState.READY + and UpgradeState.UPGRADING not in self.other_unit_states ): logger.debug( f"{top_unit.name} is next to upgrade, emitting `upgrade_granted` event and upgrading..." ) self.charm.unit.status = MaintenanceStatus("upgrading...") - self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) + self._set_state(self.charm.unit, UpgradeState.UPGRADING) try: getattr(self.on, "upgrade_granted").emit() @@ -1028,7 +1071,7 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: - MUST update unit `state` after validating the success of the upgrade, calling one of: - :class:`DataUpgrade.set_unit_failed` if the unit upgrade fails - :class:`DataUpgrade.set_unit_completed` if the unit upgrade succeeds - - MUST call :class:`DataUpgarde.on_upgrade_changed` on exit so event not lost on leader + - MUST call :class:`DataUpgrade.on_upgrade_changed` on exit so event not lost on leader """ # don't raise if k8s substrate, only return if self.substrate == "k8s": @@ -1044,7 +1087,7 @@ def _on_upgrade_finished(self, _) -> None: # Emit the upgrade relation changed event in the leader to update the upgrade_stack. if self.charm.unit.is_leader(): self.charm.on[self.relation_name].relation_changed.emit( - self.model.get_relation(self.relation_name) + self.charm.model.get_relation(self.relation_name) ) # This hook shouldn't run for the last unit (the first that is upgraded). For that unit it From a49867209f617ce2a8291340bc54d835c79bc4ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Wed, 22 Apr 2026 14:00:52 +0200 Subject: [PATCH 2/6] [K8s] Update pyproject.toml deps --- kubernetes/poetry.lock | 179 ++++++++++++++++++++++++++++---------- kubernetes/pyproject.toml | 9 +- 2 files changed, 136 insertions(+), 52 deletions(-) diff --git a/kubernetes/poetry.lock b/kubernetes/poetry.lock index 01a3b1c64..b4e71d3d1 100644 --- a/kubernetes/poetry.lock +++ b/kubernetes/poetry.lock @@ -48,6 +48,18 @@ files = [ attrs = ">=16.0.0" pluggy = ">=0.4.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main", "charm-libs"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.4.0" @@ -1296,56 +1308,129 @@ markers = {charm-libs = "platform_python_implementation != \"PyPy\""} [[package]] name = "pydantic" -version = "1.10.15" -description = "Data validation and settings management using python type hints" +version = "2.9.2" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main", "charm-libs"] files = [ - {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"}, - {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"}, - {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"}, - {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"}, - {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"}, - {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"}, - {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"}, - {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"}, - {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"}, - {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and sys_platform == \"win32\""] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +groups = ["main", "charm-libs"] +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pymacaroons" @@ -1849,14 +1934,14 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.1" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "charm-libs", "integration"] files = [ - {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, - {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [[package]] diff --git a/kubernetes/pyproject.toml b/kubernetes/pyproject.toml index 8ee8cbc34..48c4d5641 100644 --- a/kubernetes/pyproject.toml +++ b/kubernetes/pyproject.toml @@ -13,12 +13,11 @@ main = [ charm-libs = [ # data_platform_libs/v0/data_interfaces.py "ops>=2.0.0", - # data_platform_libs/v0/upgrade.py + # data_platform_libs/v1/upgrade.py "poetry-core", - # data_platform_libs/v0/upgrade.py requires pydantic ^1.10 - # data_platform_libs/v0/data_models.py requires pydantic ^1.10 - # tempo_coordinator_k8s/v0/charm_tracing.py requires pydantic - "pydantic~=1.10", + # data_platform_libs/v1/upgrade.py + # data_platform_libs/v1/data_models.py + "pydantic~=2.0", # mysql/v0/*.py" "mysql_shell_client~=0.9", # tls_certificates_interface/v1/tls_certificates.py From 3afd61f9c6e51db7e5a66ad9e79b2557233a9035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Wed, 22 Apr 2026 14:24:03 +0200 Subject: [PATCH 3/6] [K8s] Update charm code --- kubernetes/src/charm.py | 2 +- kubernetes/src/config.py | 40 +++++++++++----------- kubernetes/src/relations/mysql_provider.py | 3 +- kubernetes/src/upgrade.py | 12 +++---- kubernetes/tests/unit/test_upgrade.py | 2 +- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/kubernetes/src/charm.py b/kubernetes/src/charm.py index 17f1879fe..c661ae3af 100755 --- a/kubernetes/src/charm.py +++ b/kubernetes/src/charm.py @@ -18,8 +18,8 @@ from time import sleep import ops -from charms.data_platform_libs.v0.data_models import TypedCharmBase from charms.data_platform_libs.v0.s3 import S3Requirer +from charms.data_platform_libs.v1.data_models import TypedCharmBase from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider from charms.loki_k8s.v0.loki_push_api import LogProxyConsumer from charms.mysql.v0.async_replication import ( diff --git a/kubernetes/src/config.py b/kubernetes/src/config.py index b1b0ae20f..291d1520f 100644 --- a/kubernetes/src/config.py +++ b/kubernetes/src/config.py @@ -9,9 +9,9 @@ import re from typing import ClassVar -from charms.data_platform_libs.v0.data_models import BaseConfigModel +from charms.data_platform_libs.v1.data_models import BaseConfigModel from charms.mysql.v0.mysql import MAX_CONNECTIONS_FLOOR -from pydantic import validator +from pydantic import Field, field_validator logger = logging.getLogger(__name__) @@ -51,21 +51,21 @@ class CharmConfig(BaseConfigModel): """Manager for the structured configuration.""" profile: str - cluster_name: str | None - cluster_set_name: str | None - profile_limit_memory: int | None - mysql_interface_user: str | None - mysql_interface_database: str | None - mysql_root_interface_user: str | None - mysql_root_interface_database: str | None - experimental_max_connections: int | None + cluster_name: str | None = Field(default=None) + cluster_set_name: str | None = Field(default=None) + profile_limit_memory: int | None = Field(default=None) + mysql_interface_user: str | None = Field(default=None) + mysql_interface_database: str | None = Field(default=None) + mysql_root_interface_user: str | None = Field(default=None) + mysql_root_interface_database: str | None = Field(default=None) + experimental_max_connections: int | None = Field(default=None) binlog_retention_days: int plugin_audit_enabled: bool plugin_audit_strategy: str logs_audit_policy: str logs_retention_period: str - @validator("profile") + @field_validator("profile") @classmethod def profile_values(cls, value: str) -> str | None: """Check profile config option is one of `testing` or `production`.""" @@ -74,7 +74,7 @@ def profile_values(cls, value: str) -> str | None: return value - @validator("cluster_name", "cluster_set_name") + @field_validator("cluster_name", "cluster_set_name") @classmethod def cluster_name_validator(cls, value: str) -> str | None: """Check for valid cluster, cluster-set name. @@ -96,7 +96,7 @@ def cluster_name_validator(cls, value: str) -> str | None: return value - @validator("profile_limit_memory") + @field_validator("profile_limit_memory") @classmethod def profile_limit_memory_validator(cls, value: int) -> int | None: """Check profile limit memory.""" @@ -107,7 +107,7 @@ def profile_limit_memory_validator(cls, value: int) -> int | None: return value - @validator("mysql_interface_user", "mysql_root_interface_user") + @field_validator("mysql_interface_user", "mysql_root_interface_user") @classmethod def user_name_validator(cls, value: str) -> str | None: """Check user name is valid.""" @@ -116,7 +116,7 @@ def user_name_validator(cls, value: str) -> str | None: return value - @validator("mysql_interface_database", "mysql_root_interface_database") + @field_validator("mysql_interface_database", "mysql_root_interface_database") @classmethod def database_name_validator(cls, value: str) -> str | None: """Check database name is valid.""" @@ -128,7 +128,7 @@ def database_name_validator(cls, value: str) -> str | None: return value - @validator("experimental_max_connections") + @field_validator("experimental_max_connections") @classmethod def experimental_max_connections_validator(cls, value: int) -> int | None: """Check experimental max connections.""" @@ -140,7 +140,7 @@ def experimental_max_connections_validator(cls, value: int) -> int | None: return value - @validator("binlog_retention_days") + @field_validator("binlog_retention_days") @classmethod def binlog_retention_days_validator(cls, value: int) -> int: """Check binlog retention days.""" @@ -149,7 +149,7 @@ def binlog_retention_days_validator(cls, value: int) -> int: return value - @validator("plugin_audit_strategy") + @field_validator("plugin_audit_strategy") @classmethod def plugin_audit_strategy_validator(cls, value: str) -> str | None: """Check profile config option is one of `testing` or `production`.""" @@ -158,7 +158,7 @@ def plugin_audit_strategy_validator(cls, value: str) -> str | None: return value - @validator("logs_audit_policy") + @field_validator("logs_audit_policy") @classmethod def logs_audit_policy_validator(cls, value: str) -> str | None: """Check values for audit log policy.""" @@ -168,7 +168,7 @@ def logs_audit_policy_validator(cls, value: str) -> str | None: return value - @validator("logs_retention_period") + @field_validator("logs_retention_period") @classmethod def logs_retention_period_validator(cls, value: str) -> str: """Check logs retention period.""" diff --git a/kubernetes/src/relations/mysql_provider.py b/kubernetes/src/relations/mysql_provider.py index 98e1a26f8..000610296 100644 --- a/kubernetes/src/relations/mysql_provider.py +++ b/kubernetes/src/relations/mysql_provider.py @@ -7,6 +7,7 @@ import typing from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides, DatabaseRequestedEvent +from charms.data_platform_libs.v1.upgrade import UpgradeState from charms.mysql.v0.mysql import ( LEGACY_ROLE_ROUTER, MODERN_ROLE_ROUTER, @@ -239,7 +240,7 @@ def _on_update_status(self, _) -> None: if self.charm._is_cluster_blocked(): return - if self.charm.upgrade.state == "failed": + if self.charm.upgrade.state == UpgradeState.FAILED: # skip updating endpoints if upgrade failed # unit pod still will be labeled from another unit logger.debug("Skip labelling pods on failed upgrade") diff --git a/kubernetes/src/upgrade.py b/kubernetes/src/upgrade.py index 8648b25f5..3993026ab 100644 --- a/kubernetes/src/upgrade.py +++ b/kubernetes/src/upgrade.py @@ -7,11 +7,12 @@ import logging from typing import TYPE_CHECKING -from charms.data_platform_libs.v0.upgrade import ( +from charms.data_platform_libs.v1.upgrade import ( ClusterNotReadyError, DataUpgrade, DependencyModel, KubernetesClientError, + UpgradeState, ) from charms.mysql.v0.mysql import ( MySQLGetMySQLVersionError, @@ -24,7 +25,7 @@ ) from mysql_shell import InstanceState from ops import JujuVersion -from ops.model import BlockedStatus, MaintenanceStatus, RelationDataContent +from ops.model import BlockedStatus, MaintenanceStatus from ops.pebble import ChangeError from pydantic import BaseModel from tenacity import RetryError @@ -73,11 +74,6 @@ def highest_ordinal(self) -> int: """Return the max ordinal.""" return self.charm.app.planned_units() - 1 - @property - def unit_upgrade_data(self) -> RelationDataContent: - """Return the application upgrade data.""" - return self.peer_relation.data[self.charm.unit] - @override def pre_upgrade_check(self) -> None: """Run pre-upgrade checks.""" @@ -209,7 +205,7 @@ def _on_pebble_ready(self, event) -> None: event.defer() return - if self.state not in ["upgrading", "recovery"]: + if self.state not in [UpgradeState.UPGRADING, UpgradeState.RECOVERY]: return container = event.workload diff --git a/kubernetes/tests/unit/test_upgrade.py b/kubernetes/tests/unit/test_upgrade.py index 89b5489b9..32b1eb966 100644 --- a/kubernetes/tests/unit/test_upgrade.py +++ b/kubernetes/tests/unit/test_upgrade.py @@ -5,7 +5,7 @@ import unittest from unittest.mock import PropertyMock, call, patch -from charms.data_platform_libs.v0.upgrade import ClusterNotReadyError, KubernetesClientError +from charms.data_platform_libs.v1.upgrade import ClusterNotReadyError, KubernetesClientError from charms.mysql.v0.mysql import MySQLSetClusterPrimaryError, MySQLSetVariableError from ops.model import BlockedStatus from ops.testing import Harness From 2eb01a779219f56181b13714f964bc00c8ab3142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Wed, 22 Apr 2026 14:24:47 +0200 Subject: [PATCH 4/6] [VM] Migrate to data-platform v1 libs --- .../{v0 => v1}/data_models.py | 25 +- .../data_platform_libs/{v0 => v1}/upgrade.py | 257 ++++++++++-------- 2 files changed, 166 insertions(+), 116 deletions(-) rename machines/lib/charms/data_platform_libs/{v0 => v1}/data_models.py (95%) rename machines/lib/charms/data_platform_libs/{v0 => v1}/upgrade.py (85%) diff --git a/machines/lib/charms/data_platform_libs/v0/data_models.py b/machines/lib/charms/data_platform_libs/v1/data_models.py similarity index 95% rename from machines/lib/charms/data_platform_libs/v0/data_models.py rename to machines/lib/charms/data_platform_libs/v1/data_models.py index 087f6f3c5..9d700dbbb 100644 --- a/machines/lib/charms/data_platform_libs/v0/data_models.py +++ b/machines/lib/charms/data_platform_libs/v1/data_models.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ ```python -from charms.data_platform_libs.v0.data_models import BaseConfigModel +from charms.data_platform_libs.v1.data_models import BaseConfigModel class MyConfig(BaseConfigModel): @@ -164,20 +164,27 @@ class MergedDataBag(ProviderDataBag, RequirerDataBag): LIBID = "cb2094c5b07d47e1bf346aaee0fcfcfe" # Increment this major API version when introducing breaking changes -LIBAPI = 0 +LIBAPI = 1 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 5 +LIBPATCH = 0 -PYDEPS = ["ops>=2.0.0", "pydantic>=1.10,<2"] +PYDEPS = ["ops>=2.0.0", "pydantic>=2,<3"] G = TypeVar("G") T = TypeVar("T", bound=BaseModel) AppModel = TypeVar("AppModel", bound=BaseModel) UnitModel = TypeVar("UnitModel", bound=BaseModel) -DataBagNativeTypes = (int, str, float) +DataBagNativeTypes = ( + int, + str, + float, + Optional[int], + Optional[str], + Optional[float], +) class BaseConfigModel(BaseModel): @@ -233,7 +240,7 @@ def write(relation_data: RelationDataContent, model: BaseModel): relation_data: pointer to the relation databag model: instance of pydantic model to be written """ - for key, value in model.dict(exclude_none=False).items(): + for key, value in model.model_dump(exclude_none=False).items(): if value: relation_data[key.replace("_", "-")] = ( str(value) @@ -255,10 +262,10 @@ def read(relation_data: MutableMapping[str, str], obj: Type[T]) -> T: **{ field_name: ( relation_data[parsed_key] - if field.outer_type_ in DataBagNativeTypes + if field_info.annotation in DataBagNativeTypes else json.loads(relation_data[parsed_key]) ) - for field_name, field in obj.__fields__.items() + for field_name, field_info in obj.model_fields.items() # pyright: ignore[reportGeneralTypeIssues] if (parsed_key := field_name.replace("_", "-")) in relation_data if relation_data[parsed_key] diff --git a/machines/lib/charms/data_platform_libs/v0/upgrade.py b/machines/lib/charms/data_platform_libs/v1/upgrade.py similarity index 85% rename from machines/lib/charms/data_platform_libs/v0/upgrade.py rename to machines/lib/charms/data_platform_libs/v1/upgrade.py index 4d909d644..421cff473 100644 --- a/machines/lib/charms/data_platform_libs/v0/upgrade.py +++ b/machines/lib/charms/data_platform_libs/v1/upgrade.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -263,7 +263,8 @@ def restart(self, event) -> None: import json import logging from abc import ABC, abstractmethod -from typing import Dict, List, Literal, Optional, Set, Tuple +from enum import IntEnum +from typing import Annotated, Dict, List, Literal, Optional, Set, Tuple, Union import poetry.core.constraints.version as poetry_version from ops.charm import ( @@ -274,23 +275,46 @@ def restart(self, event) -> None: UpgradeCharmEvent, ) from ops.framework import EventBase, EventSource, Object -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, Relation, Unit, WaitingStatus -from pydantic import BaseModel, root_validator, validator +from ops.model import ( + ActiveStatus, + Application, + BlockedStatus, + MaintenanceStatus, + Relation, + Unit, + WaitingStatus, +) +from pydantic import AfterValidator, BaseModel, model_validator # The unique Charmhub library identifier, never change it LIBID = "156258aefb79435a93d933409a8c8684" # Increment this major API version when introducing breaking changes -LIBAPI = 0 +LIBAPI = 1 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 18 +LIBPATCH = 0 -PYDEPS = ["pydantic>=1.10,<2", "poetry-core"] +PYDEPS = ["pydantic>=2,<3", "poetry-core"] logger = logging.getLogger(__name__) + +class UpgradeState(IntEnum): + """Integer enumeration of upgrade states. + + Lower values represent states further behind in the upgrade process. + """ + + RECOVERY = 0 + FAILED = 1 + IDLE = 2 + READY = 3 + UPGRADING = 4 + COMPLETED = 5 + + # --- DEPENDENCY RESOLUTION FUNCTIONS --- @@ -315,6 +339,15 @@ def verify_requirements(version: str, requirement: str) -> bool: # --- DEPENDENCY MODEL TYPES --- +def validate_constraint(value: str) -> str: + """Validates a single version constraint string.""" + poetry_version.parse_constraint(value) + return value + + +VersionConstraint = Annotated[str, AfterValidator(validate_constraint)] + + class DependencyModel(BaseModel): """Manager for a single dependency. @@ -343,40 +376,23 @@ class KafkaDependenciesModel(BaseModel): model = KafkaDependenciesModel(**deps) # loading dict in to model - print(model.dict()) # exporting back validated deps + print(model.model_dump()) # exporting back validated deps """ - dependencies: Dict[str, str] + dependencies: Dict[str, VersionConstraint] name: str - upgrade_supported: str + upgrade_supported: VersionConstraint version: str - @validator("dependencies", "upgrade_supported", each_item=True) - @classmethod - def dependencies_validator(cls, value): - """Validates version constraint.""" - if isinstance(value, dict): - deps = value.values() - else: - deps = [value] - - for dep in deps: - poetry_version.parse_constraint(dep) - - return value - - @root_validator(skip_on_failure=True) - @classmethod - def version_upgrade_supported_validator(cls, values): + @model_validator(mode="after") + def version_upgrade_supported_validator(self) -> "DependencyModel": """Validates specified `version` meets `upgrade_supported` requirement.""" - if not verify_requirements( - version=values.get("version"), requirement=values.get("upgrade_supported") - ): + if not verify_requirements(version=self.version, requirement=self.upgrade_supported): raise ValueError( - f"upgrade_supported value {values.get('upgrade_supported')} greater than version value {values.get('version')} for {values.get('name')}." + f"upgrade_supported value {self.upgrade_supported} greater than version value {self.version} for {self.name}." ) - return values + return self def can_upgrade(self, dependency: "DependencyModel") -> bool: """Compares two instances of :class:`DependencyModel` for upgradability. @@ -499,8 +515,6 @@ class UpgradeEvents(CharmEvents): class DataUpgrade(Object, ABC): """Manages `upgrade` relation operations for in-place upgrades.""" - STATES = ["recovery", "failed", "idle", "ready", "upgrading", "completed"] - on = UpgradeEvents() # pyright: ignore [reportAssignmentType] def __init__( @@ -551,12 +565,9 @@ def app_units(self) -> Set[Unit]: return set([self.charm.unit] + list(self.peer_relation.units)) @property - def state(self) -> Optional[str]: + def state(self) -> Optional[UpgradeState]: """The unit state from the upgrade peer relation.""" - if not self.peer_relation: - return None - - return self.peer_relation.data[self.charm.unit].get("state", None) + return self._get_unit_state(self.charm.unit) @property def stored_dependencies(self) -> Optional[BaseModel]: @@ -567,7 +578,7 @@ def stored_dependencies(self) -> Optional[BaseModel]: if not (deps := self.peer_relation.data[self.charm.app].get("dependencies", "")): return None - return type(self.dependency_model)(**json.loads(deps)) + return type(self.dependency_model).model_validate_json(deps) @property def upgrade_stack(self) -> Optional[List[int]]: @@ -607,7 +618,7 @@ def upgrade_stack(self, stack: List[int]) -> None: self._upgrade_stack = stack @property - def other_unit_states(self) -> list: + def other_unit_states(self) -> List[Optional[UpgradeState]]: """Current upgrade state for other units. Returns: @@ -616,51 +627,77 @@ def other_unit_states(self) -> list: if not self.peer_relation: return [] - return [ - self.peer_relation.data[unit].get("state", "") - for unit in list(self.peer_relation.units) - ] + return [self._get_unit_state(unit) for unit in self.peer_relation.units] @property - def unit_states(self) -> list: + def unit_states(self) -> List[Optional[UpgradeState]]: """Current upgrade state for all units. Returns: Unsorted list of upgrade states for all units. """ - if not self.peer_relation: - return [] - - return [self.peer_relation.data[unit].get("state", "") for unit in self.app_units] + return [self._get_unit_state(unit) for unit in self.app_units] @property - def cluster_state(self) -> Optional[str]: + def cluster_state(self) -> Optional[UpgradeState]: """Current upgrade state for cluster units. - Determined from :class:`DataUpgrade.STATE`, taking the lowest ordinal unit state. + Determined taking the lowest ordinal unit state. + + For example, if units have states: `[READY, UPGRADING, COMPLETED]`, + the overall state for the cluster is `READY`. - For example, if units in have states: `["ready", "upgrading", "completed"]`, - the overall state for the cluster is `ready`. + Returns: + UpgradeState enum of the furthest behind unit, or None if invalid. + """ + states = self.unit_states + + # Build a strictly typed list to satisfy type checker of min + valid_states: List[UpgradeState] = [] + for state in states: + if state is None: + return None + valid_states.append(state) + + return min(valid_states) + + @property + def idle(self) -> bool: + """Flag for whether the cluster is in an idle upgrade state. Returns: - String of upgrade state from the furthest behind unit. + True if all application units in idle state. Otherwise, False. """ if not self.unit_states: + return False + + return set(self.unit_states) == {UpgradeState.IDLE} + + def _get_unit_state(self, unit) -> Optional[UpgradeState]: + """Helper to safely parse the state string from relation data into an Enum.""" + if not self.peer_relation: + return None + + state_str = self.peer_relation.data[unit].get("state", "") + if not state_str: return None try: - return sorted(self.unit_states, key=self.STATES.index)[0] - except (ValueError, KeyError): + return UpgradeState[state_str.upper()] + except KeyError: return None - @property - def idle(self) -> Optional[bool]: - """Flag for whether the cluster is in an idle upgrade state. + def _set_state(self, entity: Union[Unit, Application], state: UpgradeState) -> None: + """Helper to safely write an Enum state to the relation databag. - Returns: - True if all application units in idle state. Otherwise False + Args: + entity: The Juju Unit or Application to update the data for. + state: The UpgradeState enum to serialize and save. """ - return set(self.unit_states) == {"idle"} + if not self.peer_relation: + return + + self.peer_relation.data[entity].update({"state": state.name.lower()}) @abstractmethod def pre_upgrade_check(self) -> None: @@ -704,7 +741,7 @@ def _repair_upgrade_stack(self) -> None: # if the first unit in the stack fails, the stack will be the same length as units # i.e this block not ran if ( - self.cluster_state in ["failed", "recovery"] + self.cluster_state in [UpgradeState.FAILED, UpgradeState.RECOVERY] and self.upgrade_stack and len(self.upgrade_stack) != len(self.app_units) and self.charm.unit.is_leader() @@ -735,7 +772,7 @@ def set_unit_failed(self, cause: Optional[str] = None) -> None: self._upgrade_stack = None self.charm.unit.status = BlockedStatus(cause if cause else "") - self.peer_relation.data[self.charm.unit].update({"state": "failed"}) + self._set_state(self.charm.unit, UpgradeState.FAILED) self.log_rollback_instructions() def set_unit_completed(self) -> None: @@ -749,7 +786,7 @@ def set_unit_completed(self) -> None: self._upgrade_stack = None self.charm.unit.status = MaintenanceStatus("upgrade completed") - self.peer_relation.data[self.charm.unit].update({"state": "completed"}) + self._set_state(self.charm.unit, UpgradeState.COMPLETED) # Emit upgrade_finished event to run unit's post upgrade operations. if self.substrate == "k8s": @@ -765,12 +802,12 @@ def _on_upgrade_created(self, event: RelationCreatedEvent) -> None: return # setting initial idle state needed to avoid execution on upgrade-changed events - self.peer_relation.data[self.charm.unit].update({"state": "idle"}) + self._set_state(self.charm.unit, UpgradeState.IDLE) if self.charm.unit.is_leader(): logger.debug("Persisting dependencies to upgrade relation data...") self.peer_relation.data[self.charm.app].update( - {"dependencies": json.dumps(self.dependency_model.dict())} + {"dependencies": self.dependency_model.model_dump_json()} ) def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: @@ -780,18 +817,18 @@ def _on_pre_upgrade_check_action(self, event: ActionEvent) -> None: return if not self.charm.unit.is_leader(): - event.fail(message="Action must be ran on the Juju leader.") + event.fail(message="Action must be run on the Juju leader.") return - if self.cluster_state == "failed": + if self.cluster_state == UpgradeState.FAILED: logger.info("Entering recovery state for rolling-back to previous version...") self._repair_upgrade_stack() self.charm.unit.status = BlockedStatus("ready to rollback application") - self.peer_relation.data[self.charm.unit].update({"state": "recovery"}) + self._set_state(self.charm.unit, UpgradeState.RECOVERY) return # checking if upgrade in progress - if self.cluster_state != "idle": + if self.cluster_state != UpgradeState.IDLE: event.fail("Cannot run pre-upgrade checks, cluster already upgrading.") return @@ -859,32 +896,31 @@ def _upgrade_supported_check(self) -> None: Raises: :class:`VersionError` if upgrading to existing `version` is not supported """ - keys = self.dependency_model.__fields__.keys() + stored_deps = self.stored_dependencies + if not stored_deps: + return - compatible = True incompatibilities: List[Tuple[str, str, str, str]] = [] - for key in keys: - old_dep: DependencyModel = getattr(self.stored_dependencies, key) + + for key in self.dependency_model.model_fields.keys(): + old_dep: DependencyModel = getattr(stored_deps, key) new_dep: DependencyModel = getattr(self.dependency_model, key) if not old_dep.can_upgrade(dependency=new_dep): - compatible = False incompatibilities.append( (key, old_dep.version, new_dep.version, new_dep.upgrade_supported) ) - base_message = "Versions incompatible" - base_cause = "Upgrades only supported for specific versions" - if not compatible: - for incompat in incompatibilities: - base_message += ( - f", {incompat[0]} {incompat[1]} can not be upgraded to {incompat[2]}" - ) - base_cause += f", {incompat[0]} versions satisfying requirement {incompat[3]}" + if incompatibilities: + messages = [] + causes = [] + for name, old_ver, new_ver, required_ver in incompatibilities: + messages.append(f", {name} {old_ver} can not be upgraded to {new_ver}") + causes.append(f"{name} versions satisfying requirement {required_ver}") raise VersionError( - message=base_message, - cause=base_cause, + message=f"Versions incompatible, {', '.join(messages)}", + cause=f"Upgrades only supported for specific versions, {', '.join(causes)}", ) def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: @@ -911,15 +947,15 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: top_unit = self.charm.model.get_unit(f"{self.charm.app.name}/{top_unit_id}") if ( top_unit == self.charm.unit - and self.peer_relation.data[self.charm.unit].get("state") == "recovery" + and self._get_unit_state(self.charm.unit) == UpgradeState.RECOVERY ): # While in a rollback and the Juju leader unit is the top unit in the upgrade stack, emit the event # for this unit to start the rollback. - self.peer_relation.data[self.charm.unit].update({"state": "ready"}) + self._set_state(self.charm.unit, UpgradeState.READY) self.on_upgrade_changed(event) return self.charm.unit.status = WaitingStatus("other units upgrading first...") - self.peer_relation.data[self.charm.unit].update({"state": "ready"}) + self._set_state(self.charm.unit, UpgradeState.READY) if len(self.app_units) == 1: # single unit upgrade, emit upgrade_granted event right away @@ -929,7 +965,7 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: # for k8s run version checks only on highest ordinal unit if ( self.charm.unit.name - == f"{self.charm.app.name}/{self.charm.app.planned_units() -1}" + == f"{self.charm.app.name}/{self.charm.app.planned_units() - 1}" ): try: self._upgrade_supported_check() @@ -939,7 +975,7 @@ def _on_upgrade_charm(self, event: UpgradeCharmEvent) -> None: return # On K8s an unit that receives the upgrade-charm event is upgrading self.charm.unit.status = MaintenanceStatus("upgrading unit") - self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) + self._set_state(self.charm.unit, UpgradeState.UPGRADING) def on_upgrade_changed(self, event: EventBase) -> None: """Handler for `upgrade-relation-changed` events.""" @@ -947,30 +983,34 @@ def on_upgrade_changed(self, event: EventBase) -> None: return # if any other unit failed, don't continue with upgrade - if self.cluster_state == "failed": + if self.cluster_state == UpgradeState.FAILED: logger.debug("Cluster failed to upgrade, exiting...") return - if self.substrate == "vm" and self.cluster_state == "recovery": + if self.substrate == "vm" and self.cluster_state == UpgradeState.RECOVERY: # skip run while in recovery. The event will be retrigged when the cluster is ready logger.debug("Cluster in recovery, skip...") return # if all units completed, mark as complete if not self.upgrade_stack: - if self.state == "completed" and self.cluster_state in ["idle", "completed"]: + if self.state == UpgradeState.COMPLETED and self.cluster_state in [ + UpgradeState.IDLE, + UpgradeState.COMPLETED, + ]: logger.info("All units completed upgrade, setting idle upgrade state...") self.charm.unit.status = ActiveStatus() - self.peer_relation.data[self.charm.unit].update({"state": "idle"}) + + self._set_state(self.charm.unit, UpgradeState.IDLE) if self.charm.unit.is_leader(): logger.debug("Persisting new dependencies to upgrade relation data...") self.peer_relation.data[self.charm.app].update( - {"dependencies": json.dumps(self.dependency_model.dict())} + {"dependencies": self.dependency_model.model_dump_json()} ) return - if self.cluster_state == "idle": + if self.cluster_state == UpgradeState.IDLE: logger.debug("upgrade-changed event handled before pre-checks, exiting...") return @@ -978,16 +1018,19 @@ def on_upgrade_changed(self, event: EventBase) -> None: return # upgrade ongoing, set status for waiting units - if "upgrading" in self.unit_states and self.state in ["idle", "ready"]: + if UpgradeState.UPGRADING in self.unit_states and self.state in [ + UpgradeState.IDLE, + UpgradeState.READY, + ]: self.charm.unit.status = WaitingStatus("other units upgrading first...") # pop mutates the `upgrade_stack` attr top_unit_id = self.upgrade_stack.pop() top_unit = self.charm.model.get_unit(f"{self.charm.app.name}/{top_unit_id}") - top_state = self.peer_relation.data[top_unit].get("state") + top_state = self._get_unit_state(top_unit) # if top of stack is completed, leader pops it - if self.charm.unit.is_leader() and top_state == "completed": + if self.charm.unit.is_leader() and top_state == UpgradeState.COMPLETED: logger.debug(f"{top_unit} has finished upgrading, updating stack...") # writes the mutated attr back to rel data @@ -1002,15 +1045,15 @@ def on_upgrade_changed(self, event: EventBase) -> None: # if unit top of stack and all units ready (i.e stack), emit granted event if ( self.charm.unit == top_unit - and top_state in ["ready", "upgrading"] - and self.cluster_state == "ready" - and "upgrading" not in self.other_unit_states + and top_state in [UpgradeState.READY, UpgradeState.UPGRADING] + and self.cluster_state == UpgradeState.READY + and UpgradeState.UPGRADING not in self.other_unit_states ): logger.debug( f"{top_unit.name} is next to upgrade, emitting `upgrade_granted` event and upgrading..." ) self.charm.unit.status = MaintenanceStatus("upgrading...") - self.peer_relation.data[self.charm.unit].update({"state": "upgrading"}) + self._set_state(self.charm.unit, UpgradeState.UPGRADING) try: getattr(self.on, "upgrade_granted").emit() @@ -1028,7 +1071,7 @@ def _on_upgrade_granted(self, event: UpgradeGrantedEvent) -> None: - MUST update unit `state` after validating the success of the upgrade, calling one of: - :class:`DataUpgrade.set_unit_failed` if the unit upgrade fails - :class:`DataUpgrade.set_unit_completed` if the unit upgrade succeeds - - MUST call :class:`DataUpgarde.on_upgrade_changed` on exit so event not lost on leader + - MUST call :class:`DataUpgrade.on_upgrade_changed` on exit so event not lost on leader """ # don't raise if k8s substrate, only return if self.substrate == "k8s": @@ -1044,7 +1087,7 @@ def _on_upgrade_finished(self, _) -> None: # Emit the upgrade relation changed event in the leader to update the upgrade_stack. if self.charm.unit.is_leader(): self.charm.on[self.relation_name].relation_changed.emit( - self.model.get_relation(self.relation_name) + self.charm.model.get_relation(self.relation_name) ) # This hook shouldn't run for the last unit (the first that is upgraded). For that unit it From 926d2a926677cb358607e9fdcde23292f49a9133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Wed, 22 Apr 2026 14:26:04 +0200 Subject: [PATCH 5/6] [VM] Update pyproject.toml deps --- machines/poetry.lock | 192 +++++++++++++++++++++++++++++++--------- machines/pyproject.toml | 9 +- 2 files changed, 154 insertions(+), 47 deletions(-) diff --git a/machines/poetry.lock b/machines/poetry.lock index 567a6ba55..3a588eec6 100644 --- a/machines/poetry.lock +++ b/machines/poetry.lock @@ -48,6 +48,18 @@ files = [ attrs = ">=16.0.0" pluggy = ">=0.4.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main", "charm-libs"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.8.0" @@ -1304,56 +1316,137 @@ markers = {charm-libs = "platform_python_implementation != \"PyPy\""} [[package]] name = "pydantic" -version = "1.10.15" -description = "Data validation and settings management using python type hints" +version = "2.11.10" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main", "charm-libs"] files = [ - {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"}, - {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"}, - {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"}, - {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"}, - {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"}, - {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"}, - {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"}, - {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"}, - {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"}, - {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"}, + {file = "pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a"}, + {file = "pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main", "charm-libs"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyhcl" @@ -1899,6 +1992,21 @@ files = [ mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main", "charm-libs"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" version = "2.5.0" diff --git a/machines/pyproject.toml b/machines/pyproject.toml index 9b8975753..bf79337f3 100644 --- a/machines/pyproject.toml +++ b/machines/pyproject.toml @@ -14,12 +14,11 @@ main = [ charm-libs = [ # data_platform_libs/v0/data_interfaces.py "ops>=2.0.0", - # data_platform_libs/v0/upgrade.py + # data_platform_libs/v1/upgrade.py "poetry-core", - # data_platform_libs/v0/upgrade.py requires pydantic ^1.10 - # data_platform_libs/v0/data_models.py requires pydantic ^1.10 - # grafana_agent/v0/cos_agent.py requires pydantic - "pydantic~=1.10", + # data_platform_libs/v1/upgrade.py + # data_platform_libs/v1/data_models.py + "pydantic~=2.0", # grafana_agent/v0/cos_agent.py "cosl>=0.0.50", # mysql/v0/*.py" From cd1a08c5788392ab49249b1320fd9dd619a9ef23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Wed, 22 Apr 2026 16:11:14 +0200 Subject: [PATCH 6/6] [VM] Update charm code --- machines/src/charm.py | 2 +- machines/src/config.py | 32 ++++++++++++++--------------- machines/src/upgrade.py | 28 ++++++++++--------------- machines/tests/unit/test_upgrade.py | 2 +- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/machines/src/charm.py b/machines/src/charm.py index bb9a939f0..cc304f89f 100755 --- a/machines/src/charm.py +++ b/machines/src/charm.py @@ -17,8 +17,8 @@ from time import sleep import ops -from charms.data_platform_libs.v0.data_models import TypedCharmBase from charms.data_platform_libs.v0.s3 import S3Requirer +from charms.data_platform_libs.v1.data_models import TypedCharmBase from charms.grafana_agent.v0.cos_agent import COSAgentProvider, ProtocolNotFoundError from charms.mysql.v0.async_replication import ( RELATION_CONSUMER, diff --git a/machines/src/config.py b/machines/src/config.py index 63778ea41..47b735f1b 100644 --- a/machines/src/config.py +++ b/machines/src/config.py @@ -10,9 +10,9 @@ import re from typing import ClassVar -from charms.data_platform_libs.v0.data_models import BaseConfigModel +from charms.data_platform_libs.v1.data_models import BaseConfigModel from charms.mysql.v0.mysql import MAX_CONNECTIONS_FLOOR -from pydantic import validator +from pydantic import Field, field_validator logger = logging.getLogger(__name__) @@ -61,19 +61,19 @@ class CharmConfig(BaseConfigModel): """Manager for the structured configuration.""" profile: str - cluster_name: str | None - cluster_set_name: str | None - profile_limit_memory: int | None - mysql_interface_user: str | None - mysql_interface_database: str | None - experimental_max_connections: int | None + cluster_name: str | None = Field(default=None) + cluster_set_name: str | None = Field(default=None) + profile_limit_memory: int | None = Field(default=None) + mysql_interface_user: str | None = Field(default=None) + mysql_interface_database: str | None = Field(default=None) + experimental_max_connections: int | None = Field(default=None) binlog_retention_days: int plugin_audit_enabled: bool plugin_audit_strategy: str logs_audit_policy: str logs_retention_period: str - @validator("profile") + @field_validator("profile") @classmethod def profile_values(cls, value: str) -> str | None: """Check profile config option is one of `testing` or `production`.""" @@ -82,7 +82,7 @@ def profile_values(cls, value: str) -> str | None: return value - @validator("cluster_name", "cluster_set_name") + @field_validator("cluster_name", "cluster_set_name") @classmethod def cluster_name_validator(cls, value: str) -> str | None: """Check for valid cluster, cluster-set name. @@ -104,7 +104,7 @@ def cluster_name_validator(cls, value: str) -> str | None: return value - @validator("profile_limit_memory") + @field_validator("profile_limit_memory") @classmethod def profile_limit_memory_validator(cls, value: int) -> int | None: """Check profile limit memory.""" @@ -115,7 +115,7 @@ def profile_limit_memory_validator(cls, value: int) -> int | None: return value - @validator("experimental_max_connections") + @field_validator("experimental_max_connections") @classmethod def experimental_max_connections_validator(cls, value: int) -> int | None: """Check experimental max connections.""" @@ -127,7 +127,7 @@ def experimental_max_connections_validator(cls, value: int) -> int | None: return value - @validator("binlog_retention_days") + @field_validator("binlog_retention_days") @classmethod def binlog_retention_days_validator(cls, value: int) -> int: """Check binlog retention days.""" @@ -136,7 +136,7 @@ def binlog_retention_days_validator(cls, value: int) -> int: return value - @validator("plugin_audit_strategy") + @field_validator("plugin_audit_strategy") @classmethod def plugin_audit_strategy_validator(cls, value: str) -> str | None: """Check profile config option is one of `testing` or `production`.""" @@ -145,7 +145,7 @@ def plugin_audit_strategy_validator(cls, value: str) -> str | None: return value - @validator("logs_audit_policy") + @field_validator("logs_audit_policy") @classmethod def logs_audit_policy_validator(cls, value: str) -> str | None: """Check values for audit log policy.""" @@ -155,7 +155,7 @@ def logs_audit_policy_validator(cls, value: str) -> str | None: return value - @validator("logs_retention_period") + @field_validator("logs_retention_period") @classmethod def logs_retention_period_validator(cls, value: str) -> str: """Check logs retention period.""" diff --git a/machines/src/upgrade.py b/machines/src/upgrade.py index ec4d398a7..1ca265482 100644 --- a/machines/src/upgrade.py +++ b/machines/src/upgrade.py @@ -10,11 +10,12 @@ import subprocess from typing import TYPE_CHECKING -from charms.data_platform_libs.v0.upgrade import ( +from charms.data_platform_libs.v1.upgrade import ( ClusterNotReadyError, DataUpgrade, DependencyModel, UpgradeGrantedEvent, + UpgradeState, VersionError, ) from charms.mysql.v0.mysql import ( @@ -26,7 +27,6 @@ MySQLStopMySQLDError, ) from mysql_shell import InstanceState -from ops import RelationDataContent from ops.model import BlockedStatus, MaintenanceStatus, Unit from pydantic import BaseModel from typing_extensions import override @@ -72,16 +72,6 @@ def __init__(self, charm: "MySQLOperatorCharm", **kwargs) -> None: ) self.framework.observe(self.charm.on.upgrade_charm, self._on_upgrade_charm_check_legacy) - @property - def app_upgrade_data(self) -> RelationDataContent: - """Return the application upgrade data.""" - return self.peer_relation.data[self.charm.app] - - @property - def unit_upgrade_data(self) -> RelationDataContent: - """Return the application upgrade data.""" - return self.peer_relation.data[self.charm.unit] - @override def build_upgrade_stack(self) -> list[int]: """Build the upgrade stack. @@ -155,13 +145,17 @@ def _on_upgrade_charm_check_legacy(self, event) -> None: if not self.charm.unit.is_leader(): # set ready state on non-leader units - self.unit_upgrade_data.update({"state": "ready"}) + self._set_state(self.charm.unit, UpgradeState.READY) return - peers_state = list(filter(lambda state: state != "", self.unit_states)) - if len(peers_state) == len(self.peer_relation.units) and set(peers_state) == {"ready"}: + peer_states = [state for state in self.unit_states if state] + + if all(( + len(self.peer_relation.units) == len(peer_states), + all(state == UpgradeState.READY for state in peer_states), + )): # All peers have set the state to ready - self.unit_upgrade_data.update({"state": "ready"}) + self._set_state(self.charm.unit, UpgradeState.READY) self._prepare_upgrade_from_legacy() else: logger.debug("Wait until all peers have set upgrade state to ready") @@ -290,7 +284,7 @@ def _prepare_upgrade_from_legacy(self) -> None: self.upgrade_stack = upgrade_stack logger.debug("Persisting dependencies to upgrade relation data...") self.peer_relation.data[self.charm.app].update({ - "dependencies": json.dumps(self.dependency_model.dict()) + "dependencies": self.dependency_model.model_dump_json() }) @staticmethod diff --git a/machines/tests/unit/test_upgrade.py b/machines/tests/unit/test_upgrade.py index 92c022a2d..4d0808ef6 100644 --- a/machines/tests/unit/test_upgrade.py +++ b/machines/tests/unit/test_upgrade.py @@ -4,7 +4,7 @@ import unittest from unittest.mock import call, patch -from charms.data_platform_libs.v0.upgrade import ClusterNotReadyError +from charms.data_platform_libs.v1.upgrade import ClusterNotReadyError from charms.mysql.v0.mysql import ( MySQLSetClusterPrimaryError, MySQLSetVariableError,