From a98fe48698c8048b3e0411c148915791df072766 Mon Sep 17 00:00:00 2001 From: akram09 Date: Wed, 25 Feb 2026 09:25:05 +0100 Subject: [PATCH 01/15] patch: Bump dependencies --- lib/charms/opensearch/v0/models.py | 112 +++--- poetry.lock | 320 ++++++++++++++---- pyproject.toml | 16 +- .../opensearch-security/internal_users.yml | 10 +- 4 files changed, 323 insertions(+), 135 deletions(-) diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index 6e08874df0..a6b0f88af7 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -10,7 +10,7 @@ from abc import ABC from datetime import datetime from hashlib import md5 -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, Iterator, List, Literal, Optional, Union from charms.opensearch.v0.constants_secrets import ( AZURE_CREDENTIALS, @@ -18,8 +18,7 @@ S3_CREDENTIALS, ) from charms.opensearch.v0.helper_enums import BaseStrEnum -from pydantic import BaseModel, Field, root_validator, validator -from pydantic.utils import ROOT_KEY +from pydantic import BaseModel, Field, validator, model_validator, RootModel # The unique Charmhub library identifier, never change it LIBID = "6007e8030e4542e6b189e2873c8fbfef" @@ -38,8 +37,6 @@ class Model(ABC, BaseModel): """Base model class.""" def __init__(self, **data: Any) -> None: - if self.__custom_root_type__ and data.keys() != {ROOT_KEY}: - data = {ROOT_KEY: data} super().__init__(**data) def to_str(self, by_alias: bool = False) -> str: @@ -99,21 +96,22 @@ def __eq__(self, other) -> bool: class App(Model): """Data class representing an application.""" - id: Optional[str] = None - short_id: Optional[str] = None - name: Optional[str] = None - model_uuid: Optional[str] = None + id: str | None = None + short_id: str | None = None + name: str | None = None + model_uuid: str | None = None - @root_validator + @model_validator(mode="before") + @classmethod def set_props(cls, values): # noqa: N805 """Generate the attributes depending on the input.""" if None not in list(values.values()): return values - if not values["id"] and None in [values["name"], values["model_uuid"]]: + if not values.get("id") and None in [values.get("name"), values.get("model_uuid")]: raise ValueError("'id' or 'name and model_uuid' must be set.") - if values["id"]: + if values.get("id"): full_id_split = values["id"].split("/") values["name"], values["model_uuid"] = full_id_split[-1], full_id_split[0] else: @@ -122,7 +120,6 @@ def set_props(cls, values): # noqa: N805 values["short_id"] = md5(values["id"].encode()).hexdigest()[:3] return values - class Node(Model): """Data class representing a node in a cluster.""" @@ -205,12 +202,13 @@ class DeploymentState(Model): value: State message: str = Field(default="") - @root_validator + @model_validator(mode="before") + @classmethod def prevent_none(cls, values): # noqa: N805 """Validate the message or lack of depending on the state.""" - if values["value"] == State.ACTIVE: + if values.get("value") == State.ACTIVE: values["message"] = "" - elif not values["message"].strip(): + elif not values.get("message", "").strip(): raise ValueError("The message must be set when state not Active.") return values @@ -221,19 +219,20 @@ class PeerClusterConfig(Model): cluster_name: str init_hold: bool - roles: List[str] + roles: list[str] # We have a breaking change in the model # For older charms, this field will not exist and they will be set in the # profile called "testing". - data_temperature: Optional[str] = None + data_temperature: str | None = None - @root_validator + @model_validator(mode="before") + @classmethod def set_node_temperature(cls, values): # noqa: N805 """Set and validate the node temperature.""" allowed_temps = ["hot", "warm", "cold", "frozen", "content"] input_temps = set() - for role in values["roles"]: + for role in values.get("roles", []): if not role.startswith("data."): continue @@ -248,35 +247,38 @@ def set_node_temperature(cls, values): # noqa: N805 elif input_temps: temperature = input_temps.pop() values["data_temperature"] = temperature - + if not values.get("roles", []): + values["roles"] = [] values["roles"].append("data") values["roles"].remove(f"data.{temperature}") values["roles"] = list(set(values["roles"])) return values - class DeploymentDescription(Model): """Model class describing the current state of a deployment / sub-cluster.""" app: App config: PeerClusterConfig start: StartMode - pending_directives: List[Directive] + pending_directives: list[Directive] typ: DeploymentType state: DeploymentState = DeploymentState(value=State.ACTIVE) cluster_name_autogenerated: bool = False - promotion_time: Optional[float] + promotion_time: float | None = None - @root_validator + @model_validator(mode="before") + @classmethod def set_promotion_time(cls, values): # noqa: N805 """Set promotion time of a failover to a main CM.""" - if not values["promotion_time"] and values["typ"] == DeploymentType.MAIN_ORCHESTRATOR: + if ( + not values.get("promotion_time") + and values.get("typ") == DeploymentType.MAIN_ORCHESTRATOR + ): values["promotion_time"] = datetime.now().timestamp() return values - class S3RelDataCredentials(Model): """Model class for credentials passed on the PCluster relation.""" @@ -287,7 +289,7 @@ class S3RelDataCredentials(Model): class Config: """Model config of this pydantic model.""" - allow_population_by_field_name = True + validate_by_name = True class JWTAuthConfiguration(Model): @@ -322,9 +324,10 @@ class S3RelData(Model): class Config: """Model config of this pydantic model.""" - allow_population_by_field_name = True + validate_by_name = True - @root_validator + @model_validator(mode="before") + @classmethod def validate_core_fields(cls, values): # noqa: N805 """Validate the core fields of the S3 relation data.""" if ( @@ -425,7 +428,7 @@ class AzureRelDataCredentials(Model): class Config: """Model config of this pydantic model.""" - allow_population_by_field_name = True + validate_by_name = True class AzureRelData(Model): @@ -446,9 +449,10 @@ class AzureRelData(Model): class Config: """Model config of this pydantic model.""" - allow_population_by_field_name = True + validate_by_name = True - @root_validator + @model_validator(mode="before") + @classmethod def validate_core_fields(cls, values): # noqa: N805 """Validate the core fields of the azure relation data.""" if ( @@ -501,7 +505,7 @@ class GcsRelDataCredentials(Model): class Config: """Model config of this pydantic model.""" - allow_population_by_field_name = True + validate_by_name = True @validator("secret_key", pre=True) def _normalize_secret_key(cls, values): # noqa: N805 @@ -545,9 +549,10 @@ class GcsRelData(Model): class Config: """Model config of this pydantic model.""" - allow_population_by_field_name = True + validate_by_name = True - @root_validator + @model_validator(mode="before") + @classmethod def validate_core_fields(cls, values): # noqa: N805 """Validate the core fields of the gcs relation data.""" creds = values.get("credentials") @@ -606,12 +611,11 @@ class PeerClusterRelDataCredentials(Model): admin_password_hash: str kibana_password: str kibana_password_hash: str - monitor_password: Optional[str] - admin_tls: Optional[Dict[str, Optional[str]]] - s3: Optional[S3RelDataCredentials] - azure: Optional[AzureRelDataCredentials] - gcs: Optional[GcsRelDataCredentials] - + monitor_password: Optional[str] = None + admin_tls: Optional[Dict[str, Optional[str]]] = None + s3: Optional[S3RelDataCredentials] = None + azure: Optional[AzureRelDataCredentials] = None + gcs: Optional[GcsRelDataCredentials] = None class PeerClusterApp(Model): """Model class for representing an application part of a large deployment.""" @@ -622,19 +626,17 @@ class PeerClusterApp(Model): roles: List[str] -class PeerClusterFleetApps(Model): +class PeerClusterFleetApps(RootModel[dict[str, PeerClusterApp]]): """Model class for all applications in a large deployment as a dict.""" - __root__: Dict[str, PeerClusterApp] - - def __iter__(self): + def __iter__(self) -> Iterator[str]: """Implements the iter magic method.""" - return iter(self.__root__) + return iter(self.root) - def __getitem__(self, item): + def __getitem__(self, item: str) -> PeerClusterApp: """Implements the getitem magic method.""" - return self.__root__[item] - + return self.root[item] + class PluginConfigInfo(Model): """Model class for representing data needed to add or remove plugin configuration""" @@ -658,7 +660,7 @@ class PeerClusterRelData(Model): cluster_name: str cm_nodes: List[Node] credentials: PeerClusterRelDataCredentials - deployment_desc: Optional[DeploymentDescription] + deployment_desc: Optional[DeploymentDescription] = None security_index_initialised: bool = False first_data_node: Optional[str] = None plugins: Optional[Dict[str, PluginConfigInfo]] = None @@ -667,11 +669,11 @@ class PeerClusterRelData(Model): class PeerClusterRelErrorData(Model): """Model class for the PCluster relation data.""" - cluster_name: Optional[str] + cluster_name: Optional[str] = None should_sever_relation: bool should_wait: bool blocked_message: str - deployment_desc: Optional[DeploymentDescription] + deployment_desc: Optional[DeploymentDescription] = None class PeerClusterOrchestrators(Model): @@ -680,9 +682,9 @@ class PeerClusterOrchestrators(Model): _TYPES = Literal["main", "failover"] main_rel_id: int = -1 - main_app: Optional[App] + main_app: Optional[App] = None failover_rel_id: int = -1 - failover_app: Optional[App] + failover_app: Optional[App] = None def delete(self, typ: _TYPES) -> None: """Delete an orchestrator from the current pair.""" diff --git a/poetry.lock b/poetry.lock index 190bb50c0b..828ac126a1 100644 --- a/poetry.lock +++ b/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.12.1" @@ -852,18 +864,20 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "data-platform-helpers" -version = "0.1.4" +version = "0.1.7" description = "" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "data_platform_helpers-0.1.4-py3-none-any.whl", hash = "sha256:d2bd2b89198f21f7ba0229722e2c38106b7580866fe1702bc376ba768a52665a"}, - {file = "data_platform_helpers-0.1.4.tar.gz", hash = "sha256:d21a460882b7198a6e5f5129a3da432f90a8e219fc5e09852eccb436053de7f6"}, + {file = "data_platform_helpers-0.1.7-py3-none-any.whl", hash = "sha256:cf01caef414a4c07911ecf2c009c19b8f69cae154b540dd578ddbe2af8fd3b98"}, + {file = "data_platform_helpers-0.1.7.tar.gz", hash = "sha256:04445f3f4309730bb1c569a04464caeb27390b0d6e07829d09dacd8882fcfcd1"}, ] [package.dependencies] -ops = ">=2.15.0,<3.0.0" +ops = ">=2.15.0" +pydantic = ">=2.0" +rich = "*" [package.extras] all = ["pytest_operator (==0.36.0)"] @@ -1787,6 +1801,30 @@ pyRFC3339 = ">=1.0,<2.0" requests = ">=2.18.1,<3.0" six = ">=1.11.0,<2.0" +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1916,6 +1954,18 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "msal" version = "1.34.0" @@ -2049,42 +2099,42 @@ typing-extensions = ">=4.5.0" [[package]] name = "ops" -version = "2.23.1" +version = "3.5.2" description = "The Python library behind great charms" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main", "charm-libs", "integration", "unit"] files = [ - {file = "ops-2.23.1-py3-none-any.whl", hash = "sha256:fdf58163beafd25180c12a4c7efaf1e76e5f8710508a97840c07055bb78b0c77"}, - {file = "ops-2.23.1.tar.gz", hash = "sha256:aecacd67ef7ca913f63f397e0330bfa93d70529a3ef71ed2d99e2bc232564ae3"}, + {file = "ops-3.5.2-py3-none-any.whl", hash = "sha256:c715128a51ddcdf0fff463428b0f56a93e5963187e599b66594b4fc74458781b"}, + {file = "ops-3.5.2.tar.gz", hash = "sha256:849c9ed85eadf265b8a927d5e857cd112221dd71b35e4b13329ccb938c3afd18"}, ] [package.dependencies] -importlib-metadata = "*" opentelemetry-api = ">=1.0,<2.0" -ops-scenario = {version = "7.23.1", optional = true, markers = "extra == \"testing\""} +ops-scenario = {version = "8.5.2", optional = true, markers = "extra == \"testing\""} PyYAML = "==6.*" websocket-client = "==1.*" [package.extras] -testing = ["ops-scenario (==7.23.1)"] -tracing = ["ops-tracing (==2.23.1)"] +testing = ["ops-scenario (==8.5.2)"] +tracing = ["ops-tracing (==3.5.2)"] [[package]] name = "ops-scenario" -version = "7.23.1" +version = "8.5.2" description = "Python library providing a state-transition testing API for Operator Framework charms." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["unit"] files = [ - {file = "ops_scenario-7.23.1-py3-none-any.whl", hash = "sha256:fe56f4cd19fb003285505b2fe22bc5c5beda77e06496bd0ed833456b55008324"}, - {file = "ops_scenario-7.23.1.tar.gz", hash = "sha256:ae71a9a27c953b65d621495008f9f785aff28543810660063541d0b2990b2101"}, + {file = "ops_scenario-8.5.2-py3-none-any.whl", hash = "sha256:79125d82ca753394d9d9e4a53c55931d3d0114421c1b745f5611cb5827d37012"}, + {file = "ops_scenario-8.5.2.tar.gz", hash = "sha256:ebcdc4f8837f9a1cd42624f49d9d8b2502ebeeedad552516225b3420f989c369"}, ] [package.dependencies] -ops = "2.23.1" +ops = "3.5.2" PyYAML = ">=6.0.1" +typing_extensions = ">=4.9.0" [[package]] name = "overrides" @@ -2413,57 +2463,159 @@ files = [ [[package]] name = "pydantic" -version = "1.10.26" -description = "Data validation and settings management using python type hints" +version = "2.12.5" +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.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546"}, - {file = "pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572"}, - {file = "pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464"}, - {file = "pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53"}, - {file = "pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945"}, - {file = "pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637"}, - {file = "pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77"}, - {file = "pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f"}, - {file = "pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5"}, - {file = "pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df"}, - {file = "pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f"}, - {file = "pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f"}, - {file = "pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb"}, - {file = "pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b"}, - {file = "pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c"}, - {file = "pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c"}, - {file = "pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba"}, - {file = "pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62"}, - {file = "pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847"}, - {file = "pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314"}, - {file = "pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba"}, - {file = "pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37"}, - {file = "pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a"}, - {file = "pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962"}, - {file = "pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c"}, - {file = "pydantic-1.10.26-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc5c91a3b3106caf07ac6735ec6efad8ba37b860b9eb569923386debe65039ad"}, - {file = "pydantic-1.10.26-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dde599e0388e04778480d57f49355c9cc7916de818bf674de5d5429f2feebfb6"}, - {file = "pydantic-1.10.26-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8be08b5cfe88e58198722861c7aab737c978423c3a27300911767931e5311d0d"}, - {file = "pydantic-1.10.26-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0141f4bafe5eda539d98c9755128a9ea933654c6ca4306b5059fc87a01a38573"}, - {file = "pydantic-1.10.26-cp38-cp38-win_amd64.whl", hash = "sha256:eb664305ffca8a9766a8629303bb596607d77eae35bb5f32ff9245984881b638"}, - {file = "pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d"}, - {file = "pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15"}, - {file = "pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90"}, - {file = "pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba"}, - {file = "pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96"}, - {file = "pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917"}, - {file = "pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e"}, -] - -[package.dependencies] -typing-extensions = ">=4.2.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main", "charm-libs"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" [[package]] name = "pydocstyle" @@ -2519,7 +2671,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["integration", "unit"] +groups = ["main", "integration", "unit"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -2999,6 +3151,25 @@ urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] +[[package]] +name = "rich" +version = "14.3.3" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, + {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "rpds-py" version = "0.30.0" @@ -3483,6 +3654,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.6.3" @@ -3624,4 +3810,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "b1ad1fceb35b3ebb95e5a279afe9653e768ff2059e0384a55154c8fa9aec41ab" +content-hash = "fb7a16f6734d8517c295d7fe1609fba3d2eb46962cdeb79f66df0bae42dc6429" diff --git a/pyproject.toml b/pyproject.toml index f7841ef5e9..6787b5fd8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-poetry = ">=2.0.0" [tool.poetry.dependencies] python = "^3.10" -ops = "^2.22.0" +ops = "^3.1.0" tenacity = "^9.1.2" boto3 = "^1.38.36" overrides = "^7.7.0" @@ -15,10 +15,10 @@ requests = "2.32.4" # Official name: ruamel.yaml, but due to Poetry GH#109 - replace dots with dashs ruamel-yaml = "0.18.14" shortuuid = "1.0.13" -pydantic = "^1.10.17, <2" +pydantic = ">= 2.0" cryptography = "^45.0.4" jsonschema = "^4.24.0" -data-platform-helpers = "^0.1.4" +data-platform-helpers = ">=0.1.7" poetry-core = "<2.0.0" azure-storage-blob = "^12.25.0" google-cloud-storage = "^2.16.0" @@ -27,10 +27,10 @@ google-api-core = "^2.17.0" [tool.poetry.group.charm-libs.dependencies] # data_platform_libs/v0/data_interfaces.py -ops = "^2.22.0" +ops = "^3.1.0" # data_platform_libs/v0/upgrade.py # grafana_agent/v0/cos_agent.py requires pydantic <2 -pydantic = "^1.10.17, <2" +pydantic = ">=2.0" # tls_certificates_interface/v3/tls_certificates.py cryptography = "^45.0.4" jsonschema = "^4.24.0" @@ -62,8 +62,8 @@ codespell = "^2.4.1" shellcheck-py = "^0.10.0.1" [tool.poetry.group.unit.dependencies] -ops = { version = "^2.22.0", extras = ["testing"] } -ops-scenario = "^7.22.0" +ops = { version = "^3.1.0", extras = ["testing"] } +ops-scenario = "^8.5.2" pytest = "^8.4.0" pytest-asyncio = "^0.21.2" coverage = { extras = ["toml"], version = "^7.9.1" } @@ -76,7 +76,7 @@ pytest = "^8.4.0" pytest-asyncio = "^0.21.2" pytest-operator = "^0.42.0" juju = "^3.6.1.2" -ops = "^2.22.0" +ops = "^3.1.0" tenacity = "^9.1.2" pyyaml = "^6.0.2" pyjwt = "^2.10.1" diff --git a/tests/unit/resources/config/opensearch-security/internal_users.yml b/tests/unit/resources/config/opensearch-security/internal_users.yml index 0a80ae83f3..5a20051613 100644 --- a/tests/unit/resources/config/opensearch-security/internal_users.yml +++ b/tests/unit/resources/config/opensearch-security/internal_users.yml @@ -9,8 +9,12 @@ _meta: ## Demo users +kibanaserver: + hash: $2b$12$F0TalvZPzwG9enI83.G2TOaKz4nDTjcbDVVZKEA5d7L5Q65UbKFJO + reserved: false + description: Kibanaserver user admin: - hash: $2b$12$mAXCJ7AiHGDGxLiqwQ4HCefDIeyQIVsjT8zBhm2qZg6TCnBVnZ9lq + hash: $2b$12$scWc9gs4VVpzz1vSs6MZWOA2BPOMeg9X.4wqyOuI7EU94K/Iu4Ovq reserved: false backend_roles: - admin @@ -18,7 +22,3 @@ admin: - security_rest_api_access - all_access description: Admin user -kibanaserver: - hash: $2b$12$p54pyKyc0tWo.yoAlKfk5ekpLJhzEuwL./yIJURFWF5o4aXwcDVbu - reserved: false - description: Kibanaserver user From ff1fa2f12ad961ec5377c815b1f369ec4915f07f Mon Sep 17 00:00:00 2001 From: akram09 Date: Wed, 25 Feb 2026 09:53:14 +0100 Subject: [PATCH 02/15] patch: Rename `validate_by_name` to `populate_by_name` --- lib/charms/opensearch/v0/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index a6b0f88af7..e690b68f00 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -289,7 +289,7 @@ class S3RelDataCredentials(Model): class Config: """Model config of this pydantic model.""" - validate_by_name = True + populate_by_name = True class JWTAuthConfiguration(Model): @@ -324,7 +324,7 @@ class S3RelData(Model): class Config: """Model config of this pydantic model.""" - validate_by_name = True + populate_by_name = True @model_validator(mode="before") @classmethod @@ -428,7 +428,7 @@ class AzureRelDataCredentials(Model): class Config: """Model config of this pydantic model.""" - validate_by_name = True + populate_by_name = True class AzureRelData(Model): @@ -449,7 +449,7 @@ class AzureRelData(Model): class Config: """Model config of this pydantic model.""" - validate_by_name = True + populate_by_name = True @model_validator(mode="before") @classmethod @@ -505,7 +505,7 @@ class GcsRelDataCredentials(Model): class Config: """Model config of this pydantic model.""" - validate_by_name = True + populate_by_name = True @validator("secret_key", pre=True) def _normalize_secret_key(cls, values): # noqa: N805 @@ -549,7 +549,7 @@ class GcsRelData(Model): class Config: """Model config of this pydantic model.""" - validate_by_name = True + populate_by_name = True @model_validator(mode="before") @classmethod From cd79724429c5c7187a47830e76efac01bbf7bd12 Mon Sep 17 00:00:00 2001 From: akram09 Date: Wed, 25 Feb 2026 15:14:00 +0100 Subject: [PATCH 03/15] patch: Update to fix unit tests --- lib/charms/opensearch/v0/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index e690b68f00..8ba9d9d0e3 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -120,6 +120,7 @@ def set_props(cls, values): # noqa: N805 values["short_id"] = md5(values["id"].encode()).hexdigest()[:3] return values + class Node(Model): """Data class representing a node in a cluster.""" @@ -255,6 +256,7 @@ def set_node_temperature(cls, values): # noqa: N805 return values + class DeploymentDescription(Model): """Model class describing the current state of a deployment / sub-cluster.""" @@ -279,6 +281,7 @@ def set_promotion_time(cls, values): # noqa: N805 return values + class S3RelDataCredentials(Model): """Model class for credentials passed on the PCluster relation.""" @@ -617,6 +620,7 @@ class PeerClusterRelDataCredentials(Model): azure: Optional[AzureRelDataCredentials] = None gcs: Optional[GcsRelDataCredentials] = None + class PeerClusterApp(Model): """Model class for representing an application part of a large deployment.""" @@ -636,7 +640,7 @@ def __iter__(self) -> Iterator[str]: def __getitem__(self, item: str) -> PeerClusterApp: """Implements the getitem magic method.""" return self.root[item] - + class PluginConfigInfo(Model): """Model class for representing data needed to add or remove plugin configuration""" From cd7765c5aa7954b33d0a2a46df803b06ac1949d6 Mon Sep 17 00:00:00 2001 From: akram09 Date: Thu, 26 Feb 2026 09:35:47 +0100 Subject: [PATCH 04/15] patch: Fixing some issues related to migrating to pydantic v2 --- lib/charms/opensearch/v0/models.py | 169 ++++++++---------- poetry.lock | 38 +++- pyproject.toml | 1 + .../opensearch-security/internal_users.yml | 4 +- tox.ini | 2 +- 5 files changed, 117 insertions(+), 97 deletions(-) diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index 8ba9d9d0e3..2bac120923 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -18,7 +18,14 @@ S3_CREDENTIALS, ) from charms.opensearch.v0.helper_enums import BaseStrEnum -from pydantic import BaseModel, Field, validator, model_validator, RootModel +from pydantic import ( + BaseModel, + ConfigDict, + Field, + RootModel, + field_validator, + model_validator, +) # The unique Charmhub library identifier, never change it LIBID = "6007e8030e4542e6b189e2873c8fbfef" @@ -101,24 +108,24 @@ class App(Model): name: str | None = None model_uuid: str | None = None - @model_validator(mode="before") - @classmethod - def set_props(cls, values): # noqa: N805 + @model_validator(mode="after") + def set_props(self): """Generate the attributes depending on the input.""" - if None not in list(values.values()): - return values + # If all values are not None, we return self + if None not in [self.id, self.name, self.model_uuid, self.short_id]: + return self - if not values.get("id") and None in [values.get("name"), values.get("model_uuid")]: + if not self.id and None in [self.name, self.model_uuid]: raise ValueError("'id' or 'name and model_uuid' must be set.") - if values.get("id"): - full_id_split = values["id"].split("/") - values["name"], values["model_uuid"] = full_id_split[-1], full_id_split[0] + if self.id: + full_id_split = self.id.split("/") + self.name, self.model_uuid = full_id_split[-1], full_id_split[0] else: - values["id"] = f"{values['model_uuid']}/{values['name']}" + self.id = f"{self.model_uuid}/{self.name}" + self.short_id = md5(self.id.encode()).hexdigest()[:3] - values["short_id"] = md5(values["id"].encode()).hexdigest()[:3] - return values + return self class Node(Model): @@ -132,7 +139,7 @@ class Node(Model): temperature: Optional[str] = None @classmethod - @validator("roles") + @field_validator("roles") def roles_set(cls, v): """Returns deduplicated list of roles.""" return list(set(v)) @@ -203,16 +210,15 @@ class DeploymentState(Model): value: State message: str = Field(default="") - @model_validator(mode="before") - @classmethod - def prevent_none(cls, values): # noqa: N805 + @model_validator(mode="after") + def prevent_none(self): """Validate the message or lack of depending on the state.""" - if values.get("value") == State.ACTIVE: - values["message"] = "" - elif not values.get("message", "").strip(): + if self.value == State.ACTIVE: + self.message = "" + elif not self.message.strip(): raise ValueError("The message must be set when state not Active.") - return values + return self class PeerClusterConfig(Model): @@ -226,14 +232,13 @@ class PeerClusterConfig(Model): # profile called "testing". data_temperature: str | None = None - @model_validator(mode="before") - @classmethod - def set_node_temperature(cls, values): # noqa: N805 + @model_validator(mode="after") + def set_node_temperature(self): """Set and validate the node temperature.""" allowed_temps = ["hot", "warm", "cold", "frozen", "content"] input_temps = set() - for role in values.get("roles", []): + for role in self.roles: if not role.startswith("data."): continue @@ -247,14 +252,12 @@ def set_node_temperature(cls, values): # noqa: N805 raise ValueError("More than 1 data temperature provided.") elif input_temps: temperature = input_temps.pop() - values["data_temperature"] = temperature - if not values.get("roles", []): - values["roles"] = [] - values["roles"].append("data") - values["roles"].remove(f"data.{temperature}") - values["roles"] = list(set(values["roles"])) + self.data_temperature = temperature - return values + self.roles.append("data") + self.roles.remove(f"data.{temperature}") + self.roles = list(set(self.roles)) + return self class DeploymentDescription(Model): @@ -269,17 +272,13 @@ class DeploymentDescription(Model): cluster_name_autogenerated: bool = False promotion_time: float | None = None - @model_validator(mode="before") - @classmethod - def set_promotion_time(cls, values): # noqa: N805 + @model_validator(mode="after") + def set_promotion_time(self): """Set promotion time of a failover to a main CM.""" - if ( - not values.get("promotion_time") - and values.get("typ") == DeploymentType.MAIN_ORCHESTRATOR - ): - values["promotion_time"] = datetime.now().timestamp() + if not self.promotion_time and self.typ == DeploymentType.MAIN_ORCHESTRATOR: + self.promotion_time = datetime.now().timestamp() - return values + return self class S3RelDataCredentials(Model): @@ -324,40 +323,36 @@ class S3RelData(Model): credentials: S3RelDataCredentials = Field(alias=S3_CREDENTIALS) path_style_access: bool = Field(alias="s3-uri-style", default=False) - class Config: - """Model config of this pydantic model.""" + model_config = ConfigDict(populate_by_name=True) - populate_by_name = True - - @model_validator(mode="before") - @classmethod - def validate_core_fields(cls, values): # noqa: N805 + @model_validator(mode="after") + def validate_core_fields(self): """Validate the core fields of the S3 relation data.""" if ( - not (s3_creds := values.get("credentials")) - or not s3_creds.access_key - or not s3_creds.secret_key + not (self.credentials) + or not self.credentials.access_key + or not self.credentials.secret_key ): raise ValueError("Missing fields: access_key, secret_key") # NOTE: Both bucket and endpoint must be set. If none of them are set, # but credentials were found, this likely means that we are validating for a # non cluster_manager application, which only needs credentials. - if values.get("bucket") and not values.get("endpoint"): + if self.bucket and not self.endpoint: raise ValueError("Missing field: endpoint") - if values.get("endpoint") and not values.get("bucket"): + if self.endpoint and not self.bucket: raise ValueError("Missing field: bucket") - if not values.get("region"): + if not self.region: raise ValueError("Missing field: region") # remove any duplicate, prefix or trailing "/" characters - if base_path := values.get("base_path"): + if base_path := self.base_path: base_path = re.sub(r"/+", "/", base_path).strip().strip("/") - values["base_path"] = base_path or None + self.base_path = base_path or None - return values + return self - @validator("tls_ca_chain", pre=True) + @field_validator("s3_tls_ca_chain", mode="before", check_fields=False) def _tls_chain(cls, v): # noqa: N805 if v is None: return None @@ -373,14 +368,14 @@ def _tls_chain(cls, v): # noqa: N805 return json.dumps(v) return str(v) - @validator("path_style_access", pre=True) + @field_validator("path_style_access", mode="before") def change_path_style_type(cls, value) -> bool: # noqa: N805 """Coerce a type change of the path_style_access into a bool.""" if isinstance(value, str): return value.lower() == "path" return bool(value) - @validator(S3_CREDENTIALS, check_fields=False) + @field_validator(S3_CREDENTIALS, mode="before", check_fields=False) def ensure_secret_content(cls, conf: Dict[str, str] | S3RelDataCredentials): # noqa: N805 """Ensure the secret content is set.""" if not conf: @@ -449,30 +444,26 @@ class AzureRelData(Model): alias=AZURE_CREDENTIALS, default=AzureRelDataCredentials() ) - class Config: - """Model config of this pydantic model.""" - - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) - @model_validator(mode="before") - @classmethod - def validate_core_fields(cls, values): # noqa: N805 + @model_validator(mode="after") + def validate_core_fields(self): # noqa: N805 """Validate the core fields of the azure relation data.""" if ( - not (creds := values.get("credentials")) - or not creds.storage_account - or not creds.secret_key + not (self.credentials) + or not self.credentials.storage_account + or not self.credentials.secret_key ): raise ValueError("Missing fields: storage_account, secret_key") # remove any duplicate, prefix or trailing "/" characters - if base_path := values.get("base_path"): + if base_path := self.base_path: base_path = re.sub(r"/+", "/", base_path).strip().strip("/") - values["base_path"] = base_path or None + self.base_path = base_path or None - return values + return self - @validator(AZURE_CREDENTIALS, check_fields=False) + @field_validator(AZURE_CREDENTIALS, mode="before", check_fields=False) def ensure_secret_content(cls, conf: Dict[str, str] | AzureRelDataCredentials): # noqa: N805 """Ensure the secret content is set.""" if not conf: @@ -505,12 +496,9 @@ class GcsRelDataCredentials(Model): secret_key: Optional[str] = Field(alias="secret-key", default=None) - class Config: - """Model config of this pydantic model.""" + model_config = ConfigDict(populate_by_name=True) - populate_by_name = True - - @validator("secret_key", pre=True) + @field_validator("secret_key", mode="before") def _normalize_secret_key(cls, values): # noqa: N805 """Accept either raw JSON or base64-encoded JSON""" if values is None: @@ -549,30 +537,25 @@ class GcsRelData(Model): alias=GCS_CREDENTIALS, default_factory=GcsRelDataCredentials ) - class Config: - """Model config of this pydantic model.""" - - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) - @model_validator(mode="before") - @classmethod - def validate_core_fields(cls, values): # noqa: N805 + @model_validator(mode="after") + def validate_core_fields(self): """Validate the core fields of the gcs relation data.""" - creds = values.get("credentials") - if not creds or not creds.secret_key: + if not self.credentials or not self.credentials.secret_key: raise ValueError("Missing fields: secret-key") - if not values.get("bucket"): + if not self.bucket: raise ValueError("Missing field: bucket") # remove any duplicate, prefix or trailing "/" characters - if base_path := values.get("base_path"): + if base_path := self.base_path: base_path = re.sub(r"/+", "/", base_path).strip().strip("/") - values["base_path"] = base_path or None + self.base_path = base_path or None - return values + return self - @validator(GCS_CREDENTIALS, check_fields=False) + @field_validator(GCS_CREDENTIALS, mode="before", check_fields=False) def ensure_secret_content(cls, conf: Dict[str, str] | GcsRelDataCredentials): # noqa: N805): """Ensure the secret content is set.""" if not conf: diff --git a/poetry.lock b/poetry.lock index 828ac126a1..a726e8e2ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -962,6 +962,21 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["unit"] +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "executing" version = "2.2.1" @@ -2890,6 +2905,27 @@ pytest = ">=6.2.4,<10.0.0" pytest-base-url = ">=1.0.0,<3.0.0" python-slugify = ">=6.0.0,<9.0.0" +[[package]] +name = "pytest-xdist" +version = "3.8.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.9" +groups = ["unit"] +files = [ + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -3810,4 +3846,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "fb7a16f6734d8517c295d7fe1609fba3d2eb46962cdeb79f66df0bae42dc6429" +content-hash = "48e939f34c6717d13c3260c0aa5768b8c120bb5f7a88dca6f484276c7a2825a0" diff --git a/pyproject.toml b/pyproject.toml index 6787b5fd8a..61d4c8406a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ shellcheck-py = "^0.10.0.1" ops = { version = "^3.1.0", extras = ["testing"] } ops-scenario = "^8.5.2" pytest = "^8.4.0" +pytest-xdist = "^3.2.0" pytest-asyncio = "^0.21.2" coverage = { extras = ["toml"], version = "^7.9.1" } parameterized = "^0.9.0" diff --git a/tests/unit/resources/config/opensearch-security/internal_users.yml b/tests/unit/resources/config/opensearch-security/internal_users.yml index 5a20051613..bda7286bfe 100644 --- a/tests/unit/resources/config/opensearch-security/internal_users.yml +++ b/tests/unit/resources/config/opensearch-security/internal_users.yml @@ -10,11 +10,11 @@ _meta: ## Demo users kibanaserver: - hash: $2b$12$F0TalvZPzwG9enI83.G2TOaKz4nDTjcbDVVZKEA5d7L5Q65UbKFJO + hash: $2b$12$MCQjYxF3gwhQ13GTaY/c/.Q4DPPuXrG84VuuphmeVf3D3aPgUW4S. reserved: false description: Kibanaserver user admin: - hash: $2b$12$scWc9gs4VVpzz1vSs6MZWOA2BPOMeg9X.4wqyOuI7EU94K/Iu4Ovq + hash: $2b$12$R36jAEwHWWM6K8zWE9BIcOxBi8UevraE6AdqHNJcebAOdcEPQ10ya reserved: false backend_roles: - admin diff --git a/tox.ini b/tox.ini index 8de3cc2279..665743c5c0 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ commands_pre = poetry install --only main,charm-libs,unit commands = poetry run coverage run --source={[vars]src_path},{[vars]lib_path} \ - -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit + -m pytest -n 8 -v --tb native -s {posargs} {[vars]tests_path}/unit poetry run coverage report poetry run coverage xml From 9ce2b8b7b2c206b5e2366f20616e68ebed6fd0e1 Mon Sep 17 00:00:00 2001 From: akram09 Date: Thu, 26 Feb 2026 15:10:03 +0100 Subject: [PATCH 05/15] patch: Rename field validator to match field name --- lib/charms/opensearch/v0/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index 2bac120923..982e6aa756 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -352,7 +352,8 @@ def validate_core_fields(self): return self - @field_validator("s3_tls_ca_chain", mode="before", check_fields=False) + @field_validator("tls_ca_chain", mode="before", check_fields=False) + @classmethod def _tls_chain(cls, v): # noqa: N805 if v is None: return None From c3c852f4af91f5dd8f01af5e22cc28c8035314d9 Mon Sep 17 00:00:00 2001 From: akram09 Date: Fri, 27 Feb 2026 08:34:22 +0100 Subject: [PATCH 06/15] patch: Remove pytest-xdist --- pyproject.toml | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61d4c8406a..6787b5fd8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,6 @@ shellcheck-py = "^0.10.0.1" ops = { version = "^3.1.0", extras = ["testing"] } ops-scenario = "^8.5.2" pytest = "^8.4.0" -pytest-xdist = "^3.2.0" pytest-asyncio = "^0.21.2" coverage = { extras = ["toml"], version = "^7.9.1" } parameterized = "^0.9.0" diff --git a/tox.ini b/tox.ini index 665743c5c0..8de3cc2279 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ commands_pre = poetry install --only main,charm-libs,unit commands = poetry run coverage run --source={[vars]src_path},{[vars]lib_path} \ - -m pytest -n 8 -v --tb native -s {posargs} {[vars]tests_path}/unit + -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit poetry run coverage report poetry run coverage xml From 8014c1b60e8c550d862af3f74619c18d941a789e Mon Sep 17 00:00:00 2001 From: akram09 Date: Fri, 27 Feb 2026 08:37:40 +0100 Subject: [PATCH 07/15] patch: Remove pytest-xdist --- poetry.lock | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/poetry.lock b/poetry.lock index a726e8e2ee..828ac126a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -962,21 +962,6 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "execnet" -version = "2.1.2" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -groups = ["unit"] -files = [ - {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, - {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - [[package]] name = "executing" version = "2.2.1" @@ -2905,27 +2890,6 @@ pytest = ">=6.2.4,<10.0.0" pytest-base-url = ">=1.0.0,<3.0.0" python-slugify = ">=6.0.0,<9.0.0" -[[package]] -name = "pytest-xdist" -version = "3.8.0" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.9" -groups = ["unit"] -files = [ - {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, - {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -3846,4 +3810,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "48e939f34c6717d13c3260c0aa5768b8c120bb5f7a88dca6f484276c7a2825a0" +content-hash = "fb7a16f6734d8517c295d7fe1609fba3d2eb46962cdeb79f66df0bae42dc6429" From b6ac34d5bbb499927ff03200c138fd419d03812e Mon Sep 17 00:00:00 2001 From: akram09 Date: Wed, 4 Mar 2026 11:15:47 +0100 Subject: [PATCH 08/15] patch: Address review comments --- lib/charms/opensearch/v0/models.py | 111 +++++++++--------- pyproject.toml | 2 - tests/unit/lib/test_opensearch_distro.py | 4 +- .../opensearch-security/internal_users.yml | 24 ---- 4 files changed, 57 insertions(+), 84 deletions(-) delete mode 100644 tests/unit/resources/config/opensearch-security/internal_users.yml diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index 982e6aa756..664ca3798a 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -10,7 +10,7 @@ from abc import ABC from datetime import datetime from hashlib import md5 -from typing import Any, Dict, Iterator, List, Literal, Optional, Union +from typing import Any, Iterator, Literal from charms.opensearch.v0.constants_secrets import ( AZURE_CREDENTIALS, @@ -50,12 +50,12 @@ def to_str(self, by_alias: bool = False) -> str: """Deserialize object into a string.""" return json.dumps(Model.sort_payload(self.to_dict(by_alias=by_alias))) - def to_dict(self, by_alias: bool = False) -> Dict[str, Any]: + def to_dict(self, by_alias: bool = False) -> dict[str, Any]: """Deserialize object into a dict.""" return self.dict(by_alias=by_alias) @classmethod - def from_dict(cls, input_dict: Optional[Dict[str, Any]]): + def from_dict(cls, input_dict: dict[str, Any] | None): """Create a new instance of this class from a json/dict repr.""" if not input_dict: # to handle when classes defined defaults return cls() @@ -112,10 +112,10 @@ class App(Model): def set_props(self): """Generate the attributes depending on the input.""" # If all values are not None, we return self - if None not in [self.id, self.name, self.model_uuid, self.short_id]: + if None not in {self.id, self.name, self.model_uuid, self.short_id}: return self - if not self.id and None in [self.name, self.model_uuid]: + if not self.id and None in {self.name, self.model_uuid}: raise ValueError("'id' or 'name and model_uuid' must be set.") if self.id: @@ -132,14 +132,14 @@ class Node(Model): """Data class representing a node in a cluster.""" name: str - roles: List[str] + roles: list[str] ip: str app: App unit_number: int - temperature: Optional[str] = None + temperature: str | None = None - @classmethod @field_validator("roles") + @classmethod def roles_set(cls, v): """Returns deduplicated list of roles.""" return list(set(v)) @@ -286,25 +286,23 @@ class S3RelDataCredentials(Model): access_key: str = Field(alias="access-key", default=None) secret_key: str = Field(alias="secret-key", default=None) - s3_tls_ca_chain: Optional[Union[str, List[str]]] = Field(default=None, alias="s3-tls-ca-chain") + s3_tls_ca_chain: str | list[str] | None = Field(default=None, alias="s3-tls-ca-chain") - class Config: - """Model config of this pydantic model.""" + model_config = ConfigDict(populate_by_name=True) - populate_by_name = True class JWTAuthConfiguration(Model): """Model class for the configuration parameters of JWT authentication.""" signing_key: str - jwt_header: Optional[str] = None - jwt_url_parameter: Optional[str] = None + jwt_header: str | None = None + jwt_url_parameter: str | None = None roles_key: str - subject_key: Optional[str] = None - required_audience: Optional[str] = None - required_issuer: Optional[str] = None - jwt_clock_skew_tolerance_seconds: Optional[int] = None + subject_key: str | None = None + required_audience: str | None = None + required_issuer: str | None = None + jwt_clock_skew_tolerance_seconds: int | None = None class S3RelData(Model): @@ -316,10 +314,10 @@ class S3RelData(Model): bucket: str = Field(default="") endpoint: str = Field(default="") region: str = Field(default="") - base_path: Optional[str] = Field(alias="path", default=None) - protocol: Optional[str] = None - storage_class: Optional[str] = Field(alias="storage-class", default=None) - tls_ca_chain: Optional[Union[str, List[str]]] = Field(default=None, alias="tls-ca-chain") + base_path: str | None = Field(alias="path", default=None) + protocol: str | None = None + storage_class: str | None = Field(alias="storage-class", default=None) + tls_ca_chain: str | list[str] | None = Field(default=None, alias="tls-ca-chain") credentials: S3RelDataCredentials = Field(alias=S3_CREDENTIALS) path_style_access: bool = Field(alias="s3-uri-style", default=False) @@ -370,6 +368,7 @@ def _tls_chain(cls, v): # noqa: N805 return str(v) @field_validator("path_style_access", mode="before") + @classmethod def change_path_style_type(cls, value) -> bool: # noqa: N805 """Coerce a type change of the path_style_access into a bool.""" if isinstance(value, str): @@ -377,14 +376,14 @@ def change_path_style_type(cls, value) -> bool: # noqa: N805 return bool(value) @field_validator(S3_CREDENTIALS, mode="before", check_fields=False) - def ensure_secret_content(cls, conf: Dict[str, str] | S3RelDataCredentials): # noqa: N805 + @classmethod + def ensure_secret_content(cls, conf: dict[str, str] | S3RelDataCredentials): # noqa: N805 """Ensure the secret content is set.""" if not conf: return None data = conf if isinstance(conf, dict): - # We are data = S3RelDataCredentials.from_dict(conf) for value in data.dict().values(): @@ -403,7 +402,7 @@ def get_endpoint_protocol(endpoint: str) -> str: return "https" @classmethod - def from_relation(cls, input_dict: Optional[Dict[str, Any]]): + def from_relation(cls, input_dict: dict[str, Any] | None): """Create a new instance of this class from a json/dict repr. This method creates a nested S3RelDataCredentials object from the input dict. @@ -424,10 +423,7 @@ class AzureRelDataCredentials(Model): storage_account: str = Field(alias="storage-account", default=None) secret_key: str = Field(alias="secret-key", default=None) - class Config: - """Model config of this pydantic model.""" - - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) class AzureRelData(Model): @@ -438,9 +434,9 @@ class AzureRelData(Model): storage_account: str = Field(alias="storage-account", default="") container: str = Field(default="") - endpoint: Optional[str] = Field(default="") - base_path: Optional[str] = Field(alias="path", default=None) - connection_protocol: Optional[str] = Field(alias="connection-protocol", default=None) + endpoint: str | None = Field(default="") + base_path: str | None = Field(alias="path", default=None) + connection_protocol: str | None = Field(alias="connection-protocol", default=None) credentials: AzureRelDataCredentials = Field( alias=AZURE_CREDENTIALS, default=AzureRelDataCredentials() ) @@ -465,7 +461,8 @@ def validate_core_fields(self): # noqa: N805 return self @field_validator(AZURE_CREDENTIALS, mode="before", check_fields=False) - def ensure_secret_content(cls, conf: Dict[str, str] | AzureRelDataCredentials): # noqa: N805 + @classmethod + def ensure_secret_content(cls, conf: dict[str, str] | AzureRelDataCredentials): # noqa: N805 """Ensure the secret content is set.""" if not conf: return None @@ -480,7 +477,7 @@ def ensure_secret_content(cls, conf: Dict[str, str] | AzureRelDataCredentials): return data @classmethod - def from_relation(cls, input_dict: Optional[Dict[str, Any]]): + def from_relation(cls, input_dict: dict[str, Any] | None): """Create a new instance of this class from a json/dict repr. This method creates a nested AzureRelDataCredentials object from the input dict. @@ -495,11 +492,12 @@ def from_relation(cls, input_dict: Optional[Dict[str, Any]]): class GcsRelDataCredentials(Model): """Model class for credentials passed on the gcs relation.""" - secret_key: Optional[str] = Field(alias="secret-key", default=None) + secret_key: str | None = Field(alias="secret-key", default=None) model_config = ConfigDict(populate_by_name=True) @field_validator("secret_key", mode="before") + @classmethod def _normalize_secret_key(cls, values): # noqa: N805 """Accept either raw JSON or base64-encoded JSON""" if values is None: @@ -532,8 +530,8 @@ class GcsRelData(Model): """ bucket: str = Field(default="") - base_path: Optional[str] = Field(alias="path", default=None) - storage_class: Optional[str] = Field(alias="storage-class", default=None) + base_path: str | None = Field(alias="path", default=None) + storage_class: str | None = Field(alias="storage-class", default=None) credentials: GcsRelDataCredentials = Field( alias=GCS_CREDENTIALS, default_factory=GcsRelDataCredentials ) @@ -557,7 +555,8 @@ def validate_core_fields(self): return self @field_validator(GCS_CREDENTIALS, mode="before", check_fields=False) - def ensure_secret_content(cls, conf: Dict[str, str] | GcsRelDataCredentials): # noqa: N805): + @classmethod + def ensure_secret_content(cls, conf: dict[str, str] | GcsRelDataCredentials): # noqa: N805): """Ensure the secret content is set.""" if not conf: return None @@ -569,7 +568,7 @@ def ensure_secret_content(cls, conf: Dict[str, str] | GcsRelDataCredentials): # return conf @classmethod - def from_relation(cls, input_dict: Optional[Dict[str, Any]]): + def from_relation(cls, input_dict: dict[str, Any] | None): """Create a new instance of this class from a json/dict repr. This method creates a nested GcsRelDataCredentials object from the input dict. @@ -598,11 +597,11 @@ class PeerClusterRelDataCredentials(Model): admin_password_hash: str kibana_password: str kibana_password_hash: str - monitor_password: Optional[str] = None - admin_tls: Optional[Dict[str, Optional[str]]] = None - s3: Optional[S3RelDataCredentials] = None - azure: Optional[AzureRelDataCredentials] = None - gcs: Optional[GcsRelDataCredentials] = None + monitor_password: str | None = None + admin_tls: str | dict[str, str | None] | None = None + s3: str | S3RelDataCredentials | None = None + azure: str | AzureRelDataCredentials | None = None + gcs: str | GcsRelDataCredentials | None = None class PeerClusterApp(Model): @@ -610,8 +609,8 @@ class PeerClusterApp(Model): app: App planned_units: int - units: List[str] - roles: List[str] + units: list[str] + roles: list[str] class PeerClusterFleetApps(RootModel[dict[str, PeerClusterApp]]): @@ -629,8 +628,8 @@ def __getitem__(self, item: str) -> PeerClusterApp: class PluginConfigInfo(Model): """Model class for representing data needed to add or remove plugin configuration""" - relation_name: Optional[str] = None - secret_id: Optional[str] = None + relation_name: str | None = None + secret_id: str | None = None cleanup: dict[str, list[str]] = Field(default_factory=dict) def add_cleanup_items(self, cleanup: dict[str, list[str]]) -> None: @@ -646,22 +645,22 @@ class PeerClusterRelData(Model): """Model class for the PCluster relation data.""" cluster_name: str - cm_nodes: List[Node] + cm_nodes: list[Node] credentials: PeerClusterRelDataCredentials - deployment_desc: Optional[DeploymentDescription] = None + deployment_desc: DeploymentDescription | None = None security_index_initialised: bool = False - first_data_node: Optional[str] = None - plugins: Optional[Dict[str, PluginConfigInfo]] = None + first_data_node: str | None = None + plugins: dict[str, PluginConfigInfo] | None = None class PeerClusterRelErrorData(Model): """Model class for the PCluster relation data.""" - cluster_name: Optional[str] = None + cluster_name: str | None = None should_sever_relation: bool should_wait: bool blocked_message: str - deployment_desc: Optional[DeploymentDescription] = None + deployment_desc: DeploymentDescription | None = None class PeerClusterOrchestrators(Model): @@ -670,9 +669,9 @@ class PeerClusterOrchestrators(Model): _TYPES = Literal["main", "failover"] main_rel_id: int = -1 - main_app: Optional[App] = None + main_app: App | None = None failover_rel_id: int = -1 - failover_app: Optional[App] = None + failover_app: App | None = None def delete(self, typ: _TYPES) -> None: """Delete an orchestrator from the current pair.""" diff --git a/pyproject.toml b/pyproject.toml index 6787b5fd8a..fc4a303284 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,6 @@ google-api-core = "^2.17.0" [tool.poetry.group.charm-libs.dependencies] # data_platform_libs/v0/data_interfaces.py ops = "^3.1.0" -# data_platform_libs/v0/upgrade.py -# grafana_agent/v0/cos_agent.py requires pydantic <2 pydantic = ">=2.0" # tls_certificates_interface/v3/tls_certificates.py cryptography = "^45.0.4" diff --git a/tests/unit/lib/test_opensearch_distro.py b/tests/unit/lib/test_opensearch_distro.py index 5e0b9270c5..062579ca9a 100644 --- a/tests/unit/lib/test_opensearch_distro.py +++ b/tests/unit/lib/test_opensearch_distro.py @@ -62,7 +62,7 @@ def test_distro_current_online_ok(self, _): node = self.charm.opensearch.current() assert isinstance(node, Node) assert node.app.name == "opensearch" - assert node.roles == ["cluster_manager", "coordinating_only", "data", "ingest", "ml"] + assert sorted(node.roles) == ["cluster_manager", "coordinating_only", "data", "ingest", "ml"] assert not node.temperature def test_distro_current_api_unavail_primary_fallback_to_static_conf(self): @@ -104,7 +104,7 @@ def test_distro_current_api_unavail_static_conf_unavail_fallback_to_deployment(s node = self.charm.opensearch.current() assert isinstance(node, Node) assert node.app.name == "opensearch" - assert node.roles == ["data", "ingest", "ml", "cluster_manager"] + assert sorted(node.roles) == sorted(["data", "ingest", "ml", "cluster_manager"]) assert node.temperature == "warm" # We pretend that the config file is empty diff --git a/tests/unit/resources/config/opensearch-security/internal_users.yml b/tests/unit/resources/config/opensearch-security/internal_users.yml deleted file mode 100644 index bda7286bfe..0000000000 --- a/tests/unit/resources/config/opensearch-security/internal_users.yml +++ /dev/null @@ -1,24 +0,0 @@ -# This is the internal user database -# The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - -_meta: - type: internalusers - config_version: 2 - -# Define your internal users here - -## Demo users - -kibanaserver: - hash: $2b$12$MCQjYxF3gwhQ13GTaY/c/.Q4DPPuXrG84VuuphmeVf3D3aPgUW4S. - reserved: false - description: Kibanaserver user -admin: - hash: $2b$12$R36jAEwHWWM6K8zWE9BIcOxBi8UevraE6AdqHNJcebAOdcEPQ10ya - reserved: false - backend_roles: - - admin - opendistro_security_roles: - - security_rest_api_access - - all_access - description: Admin user From be25678a9be5f4abfb3b7d4e29ae5c61049c9622 Mon Sep 17 00:00:00 2001 From: akram09 Date: Wed, 4 Mar 2026 11:52:08 +0100 Subject: [PATCH 09/15] patch: Address review comments --- .../opensearch-security/internal_users.yaml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/unit/resources/config/opensearch-security/internal_users.yaml diff --git a/tests/unit/resources/config/opensearch-security/internal_users.yaml b/tests/unit/resources/config/opensearch-security/internal_users.yaml new file mode 100644 index 0000000000..0a80ae83f3 --- /dev/null +++ b/tests/unit/resources/config/opensearch-security/internal_users.yaml @@ -0,0 +1,24 @@ +# This is the internal user database +# The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh + +_meta: + type: internalusers + config_version: 2 + +# Define your internal users here + +## Demo users + +admin: + hash: $2b$12$mAXCJ7AiHGDGxLiqwQ4HCefDIeyQIVsjT8zBhm2qZg6TCnBVnZ9lq + reserved: false + backend_roles: + - admin + opendistro_security_roles: + - security_rest_api_access + - all_access + description: Admin user +kibanaserver: + hash: $2b$12$p54pyKyc0tWo.yoAlKfk5ekpLJhzEuwL./yIJURFWF5o4aXwcDVbu + reserved: false + description: Kibanaserver user From 6f7493274d458f2ce8287a398b66bd8a72a39e7c Mon Sep 17 00:00:00 2001 From: akram09 Date: Wed, 4 Mar 2026 12:25:11 +0100 Subject: [PATCH 10/15] patch: Address review comments --- .../{internal_users.yaml => internal_users.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/resources/config/opensearch-security/{internal_users.yaml => internal_users.yml} (100%) diff --git a/tests/unit/resources/config/opensearch-security/internal_users.yaml b/tests/unit/resources/config/opensearch-security/internal_users.yml similarity index 100% rename from tests/unit/resources/config/opensearch-security/internal_users.yaml rename to tests/unit/resources/config/opensearch-security/internal_users.yml From c52a97e9cb4d9fd48b220b12238f69e0a7503cb3 Mon Sep 17 00:00:00 2001 From: akram09 Date: Wed, 4 Mar 2026 12:34:57 +0100 Subject: [PATCH 11/15] patch: Address review comments --- lib/charms/opensearch/v0/models.py | 1 - tests/unit/lib/test_opensearch_distro.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index 664ca3798a..ce4728862c 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -291,7 +291,6 @@ class S3RelDataCredentials(Model): model_config = ConfigDict(populate_by_name=True) - class JWTAuthConfiguration(Model): """Model class for the configuration parameters of JWT authentication.""" diff --git a/tests/unit/lib/test_opensearch_distro.py b/tests/unit/lib/test_opensearch_distro.py index 062579ca9a..2fa99dfd2a 100644 --- a/tests/unit/lib/test_opensearch_distro.py +++ b/tests/unit/lib/test_opensearch_distro.py @@ -62,7 +62,13 @@ def test_distro_current_online_ok(self, _): node = self.charm.opensearch.current() assert isinstance(node, Node) assert node.app.name == "opensearch" - assert sorted(node.roles) == ["cluster_manager", "coordinating_only", "data", "ingest", "ml"] + assert sorted(node.roles) == [ + "cluster_manager", + "coordinating_only", + "data", + "ingest", + "ml", + ] assert not node.temperature def test_distro_current_api_unavail_primary_fallback_to_static_conf(self): From 1ac749f2c2e964d805dd8a358f3cdbb622ce45c6 Mon Sep 17 00:00:00 2001 From: akram09 Date: Fri, 6 Mar 2026 16:28:23 +0100 Subject: [PATCH 12/15] action-tmate --- .github/workflows/integration_test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index ea24b8e99b..417fd6a190 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -95,6 +95,9 @@ jobs: - name: Disk usage timeout-minutes: 1 run: df --human-readable + + - name: Setup tmate session + uses: canonical/action-tmate@main - name: Checkout timeout-minutes: 3 uses: actions/checkout@v4 From 32b3d309f41719d317236938135f287eed504a06 Mon Sep 17 00:00:00 2001 From: akram09 Date: Fri, 6 Mar 2026 16:47:14 +0100 Subject: [PATCH 13/15] action-tmate --- .github/workflows/integration_test.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 417fd6a190..8bbc355076 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -96,8 +96,6 @@ jobs: timeout-minutes: 1 run: df --human-readable - - name: Setup tmate session - uses: canonical/action-tmate@main - name: Checkout timeout-minutes: 3 uses: actions/checkout@v4 @@ -120,6 +118,10 @@ jobs: run: | sudo lxc image copy ubuntu:568f69ff1166 local: sudo lxc image alias create "juju/ubuntu@22.04/amd64" 568f69ff1166 + - name: Setup tmate session + uses: canonical/action-tmate@main + with: + detached: true - name: Run spread job timeout-minutes: 180 id: spread From 80fcbbbb16b4c55e869e0a27ca5d2ad8d29f9957 Mon Sep 17 00:00:00 2001 From: akram09 Date: Fri, 6 Mar 2026 16:48:25 +0100 Subject: [PATCH 14/15] action-tmate --- .github/workflows/integration_test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 8bbc355076..585ef6b355 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -122,6 +122,9 @@ jobs: uses: canonical/action-tmate@main with: detached: true + env: + AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} + AZURE_SECRET_KEY: ${{ secrets.AZURE_SECRET_KEY }} - name: Run spread job timeout-minutes: 180 id: spread From b581297ef28a7f4acb85107b41577085ad364f60 Mon Sep 17 00:00:00 2001 From: akram09 Date: Fri, 6 Mar 2026 17:18:25 +0100 Subject: [PATCH 15/15] remove action-tmate --- .github/workflows/integration_test.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 585ef6b355..93fbb1180b 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -118,13 +118,6 @@ jobs: run: | sudo lxc image copy ubuntu:568f69ff1166 local: sudo lxc image alias create "juju/ubuntu@22.04/amd64" 568f69ff1166 - - name: Setup tmate session - uses: canonical/action-tmate@main - with: - detached: true - env: - AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} - AZURE_SECRET_KEY: ${{ secrets.AZURE_SECRET_KEY }} - name: Run spread job timeout-minutes: 180 id: spread