diff --git a/.python-version b/.python-version index 35f236d..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12.6 +3.13 diff --git a/examples/pydantic_source/main.py b/examples/pydantic_source/main.py index 7172bce..70fb839 100644 --- a/examples/pydantic_source/main.py +++ b/examples/pydantic_source/main.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from typing import Type, Tuple, Any +from typing import Any from pydantic import Field, BaseModel from pydantic_settings import ( @@ -65,12 +65,12 @@ class RRTreeSource(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, ConfigTreeSource( @@ -121,12 +121,12 @@ class RRTreeSourceWithPrefix(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, ConfigTreeSource( @@ -204,12 +204,12 @@ class RRTreeSourceLocal(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, ConfigTreeSource( diff --git a/pydantic_source/source.py b/pydantic_source/source.py index ea3b005..9e30985 100644 --- a/pydantic_source/source.py +++ b/pydantic_source/source.py @@ -1,5 +1,6 @@ import ast -from typing import Any, Type, Dict, Iterable +from typing import Any +from collections.abc import Iterable from benedict import benedict from pathlib import Path @@ -13,7 +14,7 @@ class ConfigTreeSource(PydanticBaseSettingsSource): def __init__( self, - settings_cls: Type["BaseSettings"], + settings_cls: type["BaseSettings"], config: Configuration, tree_name: str = "default", key_prefix: str = "", @@ -43,7 +44,10 @@ def _fetch_from_api(self): key_prefixes=[self._top_prefix], with_project=self._with_project, ) - return self._extract_data_api(input_data=response["keys"].toDict()) + keys = response["keys"] + if not isinstance(keys, dict): + keys = dict(keys) + return self._extract_data_api(input_data=keys) def _load_from_local_file(self): """ @@ -74,7 +78,7 @@ def _load_config_tree(self): return processed_data # * Methods to process the tree - def _extract_data_api(self, input_data: Dict[str, Any]) -> Dict[str, Any]: + def _extract_data_api(self, input_data: dict[str, Any]) -> dict[str, Any]: return { key: self._decode_value(value.get("data")) for key, value in input_data.items() @@ -119,8 +123,8 @@ def _split_metadata(self, data: Iterable) -> Iterable: return content # * This method is extracting the data from the raw data and removing the top level prefix - def _process_config_tree(self, raw_data: Dict[str, Any]) -> Dict[str, Any]: - d: Dict[str, Any] = {} + def _process_config_tree(self, raw_data: dict[str, Any]) -> dict[str, Any]: + d: dict[str, Any] = {} prefix_length = len(self._top_prefix) if prefix_length == 0: @@ -135,7 +139,7 @@ def _process_config_tree(self, raw_data: Dict[str, Any]) -> Dict[str, Any]: def __call__(self) -> dict[str, Any]: if self.settings_cls.model_config.get("extra") == "allow": return self._configtree_data - d: Dict[str, Any] = {} + d: dict[str, Any] = {} for field_name, field in self.settings_cls.model_fields.items(): field_value, field_key, value_is_complex = self.get_field_value( diff --git a/pyproject.toml b/pyproject.toml index 44cbc22..4f30a03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,13 @@ dynamic = ["version"] description = "Python SDK for rapyuta.io v2 APIs" dependencies = [ "httpx>=0.27.2", - "munch>=4.0.0", "pydantic-settings>=2.7.1", "python-benedict>=0.34.1", "pyyaml>=6.0.2", ] readme = "README.md" license = { file = "LICENSE" } -requires-python = ">= 3.8" +requires-python = ">= 3.10" [build-system] requires = ["hatchling"] @@ -74,18 +73,15 @@ exclude = [ "venv", ] -# Same as Black. +target-version = "py310" line-length = 90 indent-width = 4 -# Assume Python 3.8 -target-version = "py38" - [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. -select = ["E4", "E7", "E9", "F", "B", "Q", "W", "N816"] +select = ["E4", "E7", "E9", "F", "B", "Q", "W", "N816", "UP"] ignore = ["E741", "B904"] # Allow fix for all enabled rules (when `--fix`) is provided. diff --git a/rapyuta_io_sdk_v2/__init__.py b/rapyuta_io_sdk_v2/__init__.py index 3f0d943..ba9380c 100644 --- a/rapyuta_io_sdk_v2/__init__.py +++ b/rapyuta_io_sdk_v2/__init__.py @@ -4,4 +4,33 @@ from rapyuta_io_sdk_v2.config import Configuration from rapyuta_io_sdk_v2.utils import walk_pages -__version__ = "0.0.1" +# Import all models directly into the main namespace +from .models import ( + # Core models + Secret, + StaticRoute, + Disk, + Deployment, + Package, + Project, + Network, + User, + Organization, + # List models + ProjectList, + DeploymentList, + DiskList, + NetworkList, + PackageList, + SecretList, + StaticRouteList, + # Managed service models + ManagedServiceProvider, + ManagedServiceBinding, + ManagedServiceBindingList, + ManagedServiceInstance, + ManagedServiceInstanceList, + ManagedServiceProviderList, +) + +__version__ = "0.3.0" diff --git a/rapyuta_io_sdk_v2/async_client.py b/rapyuta_io_sdk_v2/async_client.py index 0abd63f..f6f1916 100644 --- a/rapyuta_io_sdk_v2/async_client.py +++ b/rapyuta_io_sdk_v2/async_client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2024 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,15 +13,39 @@ # limitations under the License. import platform +from typing import Any import httpx -from munch import Munch from rapyuta_io_sdk_v2.config import Configuration -from rapyuta_io_sdk_v2.utils import handle_and_munchify_response, handle_server_errors - - -class AsyncClient(object): +from rapyuta_io_sdk_v2.models import ( + Secret, + StaticRoute, + Disk, + Deployment, + Package, + Project, + Network, + User, + ProjectList, + DeploymentList, + DiskList, + NetworkList, + PackageList, + SecretList, + StaticRouteList, + ManagedServiceBinding, + ManagedServiceBindingList, + ManagedServiceInstance, + ManagedServiceInstanceList, + ManagedServiceProviderList, + Organization, + Daemon, +) +from rapyuta_io_sdk_v2.utils import handle_server_errors + + +class AsyncClient: """AsyncClient class for the SDK.""" def __init__(self, config=None, **kwargs): @@ -37,12 +60,7 @@ def __init__(self, config=None, **kwargs): ), headers={ "User-Agent": ( - "rio-sdk-v2;N/A;{};{};{} {}".format( - platform.processor() or platform.machine(), - platform.system(), - platform.release(), - platform.version(), - ) + f"rio-sdk-v2;N/A;{platform.processor() or platform.machine()};{platform.system()};{platform.release()} {platform.version()}" ) }, ) @@ -55,12 +73,7 @@ def __init__(self, config=None, **kwargs): ), headers={ "User-Agent": ( - "rio-sdk-v2;N/A;{};{};{} {}".format( - platform.processor() or platform.machine(), - platform.system(), - platform.release(), - platform.version(), - ) + f"rio-sdk-v2;N/A;{platform.processor() or platform.machine()};{platform.system()};{platform.release()} {platform.version()}" ) }, ) @@ -77,7 +90,7 @@ def get_auth_token(self, email: str, password: str) -> str: Returns: str: authentication token """ - response = self.sync_client.post( + result = self.sync_client.post( url=f"{self.rip_host}/user/login", headers={"Content-Type": "application/json"}, json={ @@ -85,8 +98,8 @@ def get_auth_token(self, email: str, password: str) -> str: "password": password, }, ) - handle_server_errors(response) - return response.json()["data"].get("token") + handle_server_errors(result) + return result.json()["data"].get("token") def login( self, @@ -106,8 +119,7 @@ def login( token = self.get_auth_token(email, password) self.config.auth_token = token - @handle_and_munchify_response - def logout(self, token: str = None) -> Munch: + def logout(self, token: str = None) -> dict[str, Any]: """Expire the authentication token. Args: @@ -117,13 +129,15 @@ def logout(self, token: str = None) -> Munch: if token is None: token = self.config.auth_token - return self.sync_client.post( + result = self.sync_client.post( url=f"{self.rip_host}/user/logout", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", }, ) + handle_server_errors(result) + return result.json() def refresh_token(self, token: str = None, set_token: bool = True) -> str: """Refresh the authentication token. @@ -139,15 +153,15 @@ def refresh_token(self, token: str = None, set_token: bool = True) -> str: if token is None: token = self.config.auth_token - response = self.sync_client.post( + result = self.sync_client.post( url=f"{self.rip_host}/refreshtoken", headers={"Content-Type": "application/json"}, json={"token": token}, ) - handle_server_errors(response) + handle_server_errors(result) if set_token: - self.config.auth_token = response.json()["data"].get("token") - return response.json()["data"].get("token") + self.config.auth_token = result.json()["data"].get("token") + return result.json()["data"].get("token") def set_organization(self, organization_guid: str) -> None: """Set the organization GUID. @@ -166,8 +180,9 @@ def set_project(self, project_guid: str) -> None: self.config.set_project(project_guid) # -----------------Organization---------------- - @handle_and_munchify_response - async def get_organization(self, organization_guid: str = None, **kwargs) -> Munch: + async def get_organization( + self, organization_guid: str = None, **kwargs + ) -> Organization: """Get an organization by its GUID. If organization GUID is provided, the current organization GUID will be @@ -177,69 +192,80 @@ async def get_organization(self, organization_guid: str = None, **kwargs) -> Mun organization_guid (str): user provided organization GUID. Returns: - Munch: Organization details as a Munch object. + Organization: Organization details as an Organization object. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/organizations/{organization_guid}/", headers=self.config.get_headers( with_project=False, organization_guid=organization_guid, **kwargs ), ) + handle_server_errors(result) + return Organization(**result.json()) - @handle_and_munchify_response async def update_organization( - self, body: dict, organization_guid: str = None, **kwargs - ) -> Munch: + self, body: Organization | dict, organization_guid: str = None, **kwargs + ) -> Organization: """Update an organization by its GUID. Args: - body (object): Organization details + body (dict): Organization details organization_guid (str, optional): Organization GUID. Defaults to None. Returns: - Munch: Organization details as a Munch object. + Organization: Organization details as an Organization object. """ - return await self.c.put( + + if isinstance(body, dict): + body = Organization.model_validate(body) + + result = await self.c.put( url=f"{self.v2api_host}/v2/organizations/{organization_guid}/", headers=self.config.get_headers( with_project=False, organization_guid=organization_guid, **kwargs ), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Organization(**result.json()) # ---------------------User-------------------- - @handle_and_munchify_response - async def get_user(self, **kwargs) -> Munch: + async def get_user(self, **kwargs) -> User: """Get User details. Returns: - Munch: User details as a Munch object. + User: User details as a User object. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/users/me/", headers=self.config.get_headers(with_project=False, **kwargs), ) + handle_server_errors(result) + return User(**result.json()) - @handle_and_munchify_response - async def update_user(self, body: dict, **kwargs) -> Munch: + async def update_user(self, body: User | dict, **kwargs) -> User: """Update the user details. Args: body (dict): User details Returns: - Munch: User details as a Munch object. + User: User details as a User object. """ - return await self.c.put( + if isinstance(body, dict): + body = User.model_validate(body) + + result = await self.c.put( url=f"{self.v2api_host}/v2/users/me/", headers=self.config.get_headers( with_project=False, with_organization=False, **kwargs ), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return User(**result.json()) # ----------------- Projects ----------------- - @handle_and_munchify_response async def list_projects( self, cont: int = 0, @@ -247,35 +273,46 @@ async def list_projects( label_selector: list[str] = None, status: list[str] = None, organizations: list[str] = None, + name: str = None, **kwargs, - ) -> Munch: + ) -> ProjectList: """List all projects in an organization. Args: cont (int, optional): Start index of projects. Defaults to 0. limit (int, optional): Number of projects to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get projects from. Defaults to None. - status (list[str], optional): Define status to get projects from. Defaults to None. - organizations (list[str], optional): Define organizations to get projects from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get projects from. Defaults to None. + status (List[str], optional): Define status to get projects from. Defaults to None. + organizations (List[str], optional): Define organizations to get projects from. Defaults to None. + name (str, optional): Define name to get projects from. Defaults to None. Returns: - Munch: List of projects as a Munch object. + List of projects as a dictionary. """ - return await self.c.get( + parameters = { + "continue": cont, + "limit": limit, + } + if organizations: + parameters["organizations"] = organizations + if label_selector: + parameters["labelSelector"] = label_selector + if status: + parameters["status"] = status + if name: + parameters["name"] = name + + result = await self.c.get( url=f"{self.v2api_host}/v2/projects/", headers=self.config.get_headers(with_project=False, **kwargs), - params={ - "continue": cont, - "limit": limit, - "status": status, - "organizations": organizations, - "labelSelector": label_selector, - }, + params=parameters, ) - @handle_and_munchify_response - async def get_project(self, project_guid: str = None, **kwargs) -> Munch: + handle_server_errors(result) + return ProjectList(**result.json()) + + async def get_project(self, project_guid: str = None, **kwargs) -> Project: """Get a project by its GUID. If no project or organization GUID is provided, @@ -289,87 +326,104 @@ async def get_project(self, project_guid: str = None, **kwargs) -> Munch: ValueError: If organization_guid or project_guid is None Returns: - Munch: Project details as a Munch object. + Project details as a dictionary. """ if project_guid is None: project_guid = self.config.project_guid - if not project_guid: raise ValueError("project_guid is required") - - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/projects/{project_guid}/", headers=self.config.get_headers(with_project=False, **kwargs), ) + handle_server_errors(result) + return Project(**result.json()) - @handle_and_munchify_response - async def create_project(self, body: dict, **kwargs) -> Munch: + async def create_project(self, body: Project | dict, **kwargs) -> Project: """Create a new project. Args: - body (object): Project details + body (dict): Project details Returns: - Munch: Project details as a Munch object. + Project details as a dictionary. """ + if isinstance(body, dict): + body = Project.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/projects/", headers=self.config.get_headers(with_project=False, **kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Project(**result.json()) - @handle_and_munchify_response async def update_project( - self, body: dict, project_guid: str = None, **kwargs - ) -> Munch: + self, body: Project | dict, project_guid: str = None, **kwargs + ) -> Project: """Update a project by its GUID. + Args: + body (dict): Project details + project_guid (str, optional): Project GUID. Defaults to None. + Returns: - Munch: Project details as a Munch object. + Project details as a dictionary. """ + if isinstance(body, dict): + body = Project.model_validate(body) - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/projects/{project_guid}/", headers=self.config.get_headers(with_project=False, **kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Project(**result.json()) - @handle_and_munchify_response - async def delete_project(self, project_guid: str, **kwargs) -> Munch: + async def delete_project(self, project_guid: str, **kwargs) -> None: """Delete a project by its GUID. Args: project_guid (str): Project GUID Returns: - Munch: Project details as a Munch object. + None if successful. """ - - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/projects/{project_guid}/", headers=self.config.get_headers(with_project=False, **kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response async def update_project_owner( - self, body: dict, project_guid: str = None, **kwargs - ) -> Munch: + self, body: Project | dict, project_guid: str = None, **kwargs + ) -> dict[str, Any]: """Update the owner of a project by its GUID. + Args: + body (dict): Project details + project_guid (str, optional): Project GUID. Defaults to None. + Returns: - Munch: Project details as a Munch object. + Project details as a dictionary. """ project_guid = project_guid or self.config.project_guid - return await self.c.put( + if isinstance(body, dict): + body = Project.model_validate(body) + + result = await self.c.put( url=f"{self.v2api_host}/v2/projects/{project_guid}/owner/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Project(**result.json()) # -------------------Package------------------- - @handle_and_munchify_response async def list_packages( self, cont: int = 0, @@ -377,20 +431,19 @@ async def list_packages( label_selector: list[str] = None, name: str = None, **kwargs, - ) -> Munch: + ) -> PackageList: """List all packages in a project. Args: cont (int, optional): Start index of packages. Defaults to 0. limit (int, optional): Number of packages to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get packages from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get packages from. Defaults to None. name (str, optional): Define name to get packages from. Defaults to None. Returns: - Munch: List of packages as a Munch object. + List of packages as a dictionary. """ - - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/packages/", headers=self.config.get_headers(**kwargs), params={ @@ -401,25 +454,34 @@ async def list_packages( }, ) - @handle_and_munchify_response - async def create_package(self, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + return PackageList(**result.json()) + + async def create_package(self, body: Package | dict, **kwargs) -> Package: """Create a new package. The Payload is the JSON format of the Package Manifest. For a documented example, run the rio explain package command. + Args: + body (dict): Package details + Returns: - Munch: Package details as a Munch object. + Package: Package details as a Package object. """ + if isinstance(body, dict): + body = Package.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/packages/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - async def get_package(self, name: str, version: str = None, **kwargs) -> Munch: + handle_server_errors(result) + return Package(**result.json()) + + async def get_package(self, name: str, version: str = None, **kwargs) -> Package: """Get a package by its name. Args: @@ -427,32 +489,35 @@ async def get_package(self, name: str, version: str = None, **kwargs) -> Munch: version (str, optional): Package version. Defaults to None. Returns: - Munch: Package details as a Munch object. + Package: Package details as a Package object. """ - - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/packages/{name}/", headers=self.config.get_headers(**kwargs), params={"version": version}, ) + handle_server_errors(result) - @handle_and_munchify_response - async def delete_package(self, name: str, **kwargs) -> Munch: + return Package(**result.json()) + + async def delete_package(self, name: str, version: str, **kwargs) -> None: """Delete a package by its name. Args: name (str): Package name Returns: - Munch: Package details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/packages/{name}/", headers=self.config.get_headers(**kwargs), + params={"version": version}, ) + handle_server_errors(result) + return None - @handle_and_munchify_response async def list_deployments( self, cont: int = 0, @@ -468,7 +533,7 @@ async def list_deployments( phases: list[str] = None, regions: list[str] = None, **kwargs, - ) -> Munch: + ) -> DeploymentList: """List all deployments in a project. Args: @@ -476,20 +541,20 @@ async def list_deployments( limit (int, optional): Number of deployments to list. Defaults to 50. dependencies (bool, optional): Filter by dependencies. Defaults to False. device_name (str, optional): Filter deployments by device name. Defaults to None. - guids (list[str], optional): Filter by GUIDs. Defaults to None. - label_selector (list[str], optional): Define labelSelector to get deployments from. Defaults to None. + guids (List[str], optional): Filter by GUIDs. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get deployments from. Defaults to None. name (str, optional): Define name to get deployments from. Defaults to None. - names (list[str], optional): Define names to get deployments from. Defaults to None. + names (List[str], optional): Define names to get deployments from. Defaults to None. package_name (str, optional): Filter by package name. Defaults to None. package_version (str, optional): Filter by package version. Defaults to None. - phases (list[str], optional): Filter by phases. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. - regions (list[str], optional): Filter by regions. Defaults to None. + phases (List[str], optional): Filter by phases. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. + regions (List[str], optional): Filter by regions. Defaults to None. Returns: - Munch: List of deployments as a Munch object. + List of deployments as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/deployments/", headers=self.config.get_headers(**kwargs), params={ @@ -508,97 +573,137 @@ async def list_deployments( }, ) + handle_server_errors(response=result) + + return DeploymentList(**result.json()) + # -------------------Deployment------------------- - @handle_and_munchify_response - async def create_deployment(self, body: dict, **kwargs) -> Munch: + async def create_deployment(self, body: Deployment | dict, **kwargs) -> Deployment: """Create a new deployment. Args: - body (object): Deployment details + body (dict): Deployment details Returns: - Munch: Deployment details as a Munch object. + Deployment: Deployment details as a Deployment object. """ + if isinstance(body, dict): + body = Deployment.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/deployments/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - async def get_deployment(self, name: str, guid: str = None, **kwargs) -> Munch: + handle_server_errors(result) + return Deployment(**result.json()) + + async def get_deployment(self, name: str, guid: str = None, **kwargs) -> Deployment: """Get a deployment by its name. + Args: + name (str): Deployment name + guid (str, optional): Deployment GUID. Defaults to None. + Returns: - Munch: Deployment details as a Munch object. + Deployment: Deployment details as a Deployment object. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/deployments/{name}/", headers=self.config.get_headers(**kwargs), params={"guid": guid}, ) - @handle_and_munchify_response - async def update_deployment(self, name: str, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + + return Deployment(**result.json()) + + async def update_deployment( + self, name: str, body: Deployment | dict, **kwargs + ) -> Deployment: """Update a deployment by its name. + Args: + name (str): Deployment name + body (dict): Deployment details + Returns: - Munch: Deployment details as a Munch object. + Deployment: Deployment details as a Deployment object. """ + if isinstance(body, dict): + body = Deployment.model_validate(body) - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/deployments/{name}/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Deployment(**result.json()) - @handle_and_munchify_response - async def delete_deployment(self, name: str, **kwargs) -> Munch: + async def delete_deployment(self, name: str, **kwargs) -> None: """Delete a deployment by its name. Returns: - Munch: Deployment details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/deployments/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response - async def get_deployment_graph(self, name: str, **kwargs) -> Munch: + async def get_deployment_graph(self, name: str, **kwargs) -> dict[str, Any]: """Get a deployment graph by its name. [Experimental] Returns: - Munch: Deployment graph as a Munch object. + Deployment graph as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/deployments/{name}/graph/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response async def get_deployment_history( self, name: str, guid: str = None, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Get a deployment history by its name. Returns: - Munch: Deployment history as a Munch object. + Deployment history as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/deployments/{name}/history/", headers=self.config.get_headers(**kwargs), params={"guid": guid}, ) + handle_server_errors(result) + return result.json() + + async def stream_deployment_logs(self, name: str, executable: str, replica: int = 0): + """Asynchronously stream logs for a deployment executable replica.""" + url = f"{self.v2api_host}/v2/deployments/{name}/logs/?replica={replica}&executable={executable}" + + async with self.c.stream( + "GET", url=url, headers=self.config.get_headers() + ) as response: + response.raise_for_status() + + async for line in response.aiter_lines(): + if line: + yield line # -------------------Disks------------------- - @handle_and_munchify_response + async def list_disks( self, cont: int = 0, @@ -608,23 +713,23 @@ async def list_disks( regions: list[str] = None, status: list[str] = None, **kwargs, - ) -> Munch: + ) -> DiskList: """List all disks in a project. Args: cont (int, optional): Start index of disks. Defaults to 0. - label_selector (list[str], optional): Define labelSelector to get disks from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get disks from. Defaults to None. limit (int, optional): Number of disks to list. Defaults to 50. - names (list[str], optional): Define names to get disks from. Defaults to None. - regions (list[str], optional): Define regions to get disks from. Defaults to None. - status (list[str], optional): Define status to get disks from. Available values : Available, Bound, Released, Failed, Pending.Defaults to None. + names (List[str], optional): Define names to get disks from. Defaults to None. + regions (List[str], optional): Define regions to get disks from. Defaults to None. + status (List[str], optional): Define status to get disks from. Available values : Available, Bound, Released, Failed, Pending.Defaults to None. Returns: - Munch: List of disks as a Munch object. + List of disks as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/disks/", headers=self.config.get_headers(**kwargs), params={ @@ -637,54 +742,86 @@ async def list_disks( }, ) - @handle_and_munchify_response - async def get_disk(self, name: str, **kwargs) -> Munch: + handle_server_errors(response=result) + return DiskList(**result.json()) + + async def get_disk(self, name: str, **kwargs) -> Disk: """Get a disk by its name. Args: name (str): Disk name Returns: - Munch: Disk details as a Munch object. + Disk: Disk details as a Disk object. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/disks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(response=result) + + return Disk(**result.json()) - @handle_and_munchify_response - async def create_disk(self, body: str, **kwargs) -> Munch: + async def create_disk(self, body: Disk | dict, **kwargs) -> Disk: """Create a new disk. + Args: + body (dict): Disk details + Returns: - Munch: Disk details as a Munch object. + Disk: Disk details as a Disk object. """ + if isinstance(body, dict): + body = Disk.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/disks/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Disk(**result.json()) - @handle_and_munchify_response - async def delete_disk(self, name: str, **kwargs) -> Munch: + async def delete_disk(self, name: str, **kwargs) -> None: """Delete a disk by its name. Args: name (str): Disk name Returns: - Munch: Disk details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/disks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None + + # -------------------Device-------------------------- + + async def get_device_daemons(self, device_guid: str): + """ + Retrieve the list of daemons associated with a specific device. + + Args: + device_guid (str): The unique identifier (GUID) of the device. + + Returns: + dict: The JSON response containing information about the device's daemons. + """ + result = await self.c.get( + url=f"{self.v2api_host}/v2/devices/daemons/{device_guid}/", + headers=self.config.get_headers(), + ) + + handle_server_errors(response=result) + return Daemon(**result.json()) # -------------------Static Routes------------------- - @handle_and_munchify_response + async def list_staticroutes( self, cont: int = 0, @@ -694,22 +831,22 @@ async def list_staticroutes( names: list[str] = None, regions: list[str] = None, **kwargs, - ) -> Munch: + ) -> StaticRouteList: """List all static routes in a project. Args: cont (int, optional): Start index of static routes. Defaults to 0. limit (int, optional): Number of static routes to list. Defaults to 50. - guids (list[str], optional): Define guids to get static routes from. Defaults to None. - label_selector (list[str], optional): Define labelSelector to get static routes from. Defaults to None. - names (list[str], optional): Define names to get static routes from. Defaults to None. - regions (list[str], optional): Define regions to get static routes from. Defaults to None. + guids (List[str], optional): Define guids to get static routes from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get static routes from. Defaults to None. + names (List[str], optional): Define names to get static routes from. Defaults to None. + regions (List[str], optional): Define regions to get static routes from. Defaults to None. Returns: - Munch: List of static routes as a Munch object. + List of static routes as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/staticroutes/", headers=self.config.get_headers(**kwargs), params={ @@ -722,38 +859,50 @@ async def list_staticroutes( }, ) - @handle_and_munchify_response - async def create_staticroute(self, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + return StaticRouteList(**result.json()) + + async def create_staticroute(self, body: StaticRoute | dict, **kwargs) -> StaticRoute: """Create a new static route. + Args: + body (dict): Static route details + Returns: - Munch: Static route details as a Munch object. + StaticRoute: Static route details as a StaticRoute object. """ + if isinstance(body, dict): + body = StaticRoute.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/staticroutes/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return StaticRoute(**result.json()) - @handle_and_munchify_response - async def get_staticroute(self, name: str, **kwargs) -> Munch: + async def get_staticroute(self, name: str, **kwargs) -> StaticRoute: """Get a static route by its name. Args: name (str): Static route name Returns: - Munch: Static route details as a Munch object. + StaticRoute: Static route details as a StaticRoute object. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/staticroutes/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(response=result) + + return StaticRoute(**result.json()) - @handle_and_munchify_response - async def update_staticroute(self, name: str, body: dict, **kwargs) -> Munch: + async def update_staticroute( + self, name: str, body: StaticRoute | dict, **kwargs + ) -> StaticRoute: """Update a static route by its name. Args: @@ -761,33 +910,38 @@ async def update_staticroute(self, name: str, body: dict, **kwargs) -> Munch: body (dict): Update details Returns: - Munch: Static route details as a Munch object. + StaticRoute: Static route details as a StaticRoute object. """ + if isinstance(body, dict): + body = StaticRoute.model_validate(body) - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/staticroutes/{name}/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return StaticRoute(**result.json()) - @handle_and_munchify_response - async def delete_staticroute(self, name: str, **kwargs) -> Munch: + async def delete_staticroute(self, name: str, **kwargs) -> None: """Delete a static route by its name. Args: name (str): Static route name Returns: - Munch: Static route details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/staticroutes/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None # -------------------Networks------------------- - @handle_and_munchify_response + async def list_networks( self, cont: int = 0, @@ -800,25 +954,25 @@ async def list_networks( regions: list[str] = None, status: list[str] = None, **kwargs, - ) -> Munch: + ) -> NetworkList: """List all networks in a project. Args: cont (int, optional): Start index of networks. Defaults to 0. limit (int, optional): Number of networks to list. Defaults to 50. device_name (str, optional): Filter networks by device name. Defaults to None. - label_selector (list[str], optional): Define labelSelector to get networks from. Defaults to None. - names (list[str], optional): Define names to get networks from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get networks from. Defaults to None. + names (List[str], optional): Define names to get networks from. Defaults to None. network_type (str, optional): Define network type to get networks from. Defaults to None. - phases (list[str], optional): Define phases to get networks from. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. - regions (list[str], optional): Define regions to get networks from. Defaults to None. - status (list[str], optional): Define status to get networks from. Available values : Running, Pending, Error, Unknown, Stopped. Defaults to None. + phases (List[str], optional): Define phases to get networks from. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. + regions (List[str], optional): Define regions to get networks from. Defaults to None. + status (List[str], optional): Define status to get networks from. Available values : Running, Pending, Error, Unknown, Stopped. Defaults to None. Returns: - Munch: List of networks as a Munch object. + List of networks as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/networks/", headers=self.config.get_headers(**kwargs), params={ @@ -834,54 +988,66 @@ async def list_networks( }, ) - @handle_and_munchify_response - async def create_network(self, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + return NetworkList(**result.json()) + + async def create_network(self, body: Network | dict, **kwargs) -> Network: """Create a new network. + Args: + body (dict): Network details + Returns: - Munch: Network details as a Munch object. + Network: Network details as a Network object. """ + if isinstance(body, dict): + body = Network.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/networks/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Network(**result.json()) - @handle_and_munchify_response - async def get_network(self, name: str, **kwargs) -> Munch: + async def get_network(self, name: str, **kwargs) -> Network: """Get a network by its name. Args: name (str): Network name Returns: - Munch: Network details as a Munch object. + Network: Network details as a Network object. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/networks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(response=result) + + return Network(**result.json()) - @handle_and_munchify_response - async def delete_network(self, name: str, **kwargs) -> Munch: + async def delete_network(self, name: str, **kwargs) -> None: """Delete a network by its name. Args: name (str): Network name Returns: - Munch: Network details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/networks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None # -------------------Secrets------------------- - @handle_and_munchify_response + async def list_secrets( self, cont: int = 0, @@ -890,64 +1056,80 @@ async def list_secrets( names: list[str] = None, regions: list[str] = None, **kwargs, - ) -> Munch: + ) -> SecretList: """List all secrets in a project. Args: cont (int, optional): Start index of secrets. Defaults to 0. limit (int, optional): Number of secrets to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get secrets from. Defaults to None. - names (list[str], optional): Define names to get secrets from. Defaults to None. - regions (list[str], optional): Define regions to get secrets from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get secrets from. Defaults to None. + names (List[str], optional): Define names to get secrets from. Defaults to None. + regions (List[str], optional): Define regions to get secrets from. Defaults to None. Returns: - Munch: List of secrets as a Munch object. + List of secrets as a dictionary. """ - return await self.c.get( + parameters = { + "continue": cont, + "limit": limit, + } + if label_selector is not None: + parameters["labelSelector"] = label_selector + if names is not None: + parameters["names"] = names + if regions is not None: + parameters["regions"] = regions + + result = await self.c.get( url=f"{self.v2api_host}/v2/secrets/", headers=self.config.get_headers(**kwargs), - params={ - "continue": cont, - "limit": limit, - "labelSelector": label_selector, - "names": names, - "regions": regions, - }, + params=parameters, ) - @handle_and_munchify_response - async def create_secret(self, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + return SecretList(**result.json()) + + async def create_secret(self, body: Secret | dict, **kwargs) -> Secret: """Create a new secret. + Args: + body (dict): Secret details + Returns: - Munch: Secret details as a Munch object. + Secret: Secret details as a Secret object. """ + if isinstance(body, dict): + body = Secret.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/secrets/", - headers=self.config.get_headers(*kwargs), - json=body, + headers=self.config.get_headers(**kwargs), + json=body.model_dump(), ) - @handle_and_munchify_response - async def get_secret(self, name: str, **kwargs) -> Munch: + handle_server_errors(result) + return Secret(**result.json()) + + async def get_secret(self, name: str, **kwargs) -> Secret: """Get a secret by its name. Args: name (str): Secret name Returns: - Munch: Secret details as a Munch object. + Secret: Secret details as a Secret object. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/secrets/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(response=result) + + return Secret(**result.json()) - @handle_and_munchify_response - async def update_secret(self, name: str, body: dict, **kwargs) -> Munch: + async def update_secret(self, name: str, body: Secret | dict, **kwargs) -> Secret: """Update a secret by its name. Args: @@ -955,33 +1137,168 @@ async def update_secret(self, name: str, body: dict, **kwargs) -> Munch: body (dict): Update details Returns: - Munch: Secret details as a Munch object. + Secret: Secret details as a Secret object. """ + if isinstance(body, dict): + body = Secret.model_validate(body) - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/secrets/{name}/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Secret(**result.json()) - @handle_and_munchify_response - async def delete_secret(self, name: str, **kwargs) -> Munch: + async def delete_secret(self, name: str, **kwargs) -> None: """Delete a secret by its name. Args: name (str): Secret name Returns: - Munch: Secret details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/secrets/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None + + # -------------------OAuth2 Clients------------------- + async def list_oauth2_clients( + self, + cont: int = 0, + limit: int = 50, + label_selector: list[str] = None, + names: list[str] = None, + regions: list[str] = None, + **kwargs, + ) -> dict[str, Any]: + """List all OAuth2 clients in a project. + + Args: + cont (int, optional): Start index. Defaults to 0. + limit (int, optional): Number to list. Defaults to 50. + label_selector (List[str], optional): Label selector. Defaults to None. + names (List[str], optional): Names filter. Defaults to None. + regions (List[str], optional): Regions filter. Defaults to None. + + Returns: + List of OAuth2 clients as a dictionary. + """ + params = { + "continue": cont, + "limit": limit, + } + if label_selector is not None: + params["labelSelector"] = label_selector + if names is not None: + params["names"] = names + if regions is not None: + params["regions"] = regions + + result = await self.c.get( + url=f"{self.v2api_host}/v2/oauth2clients/", + headers=self.config.get_headers(**kwargs), + params=params, + ) + handle_server_errors(result) + return result.json() + + async def get_oauth2_client(self, client_id: str, **kwargs) -> dict[str, Any]: + """Get an OAuth2 client by its client_id. + + Args: + client_id (str): OAuth2 client ID + + Returns: + OAuth2 client details as a dictionary. + """ + result = await self.c.get( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/", + headers=self.config.get_headers(**kwargs), + ) + handle_server_errors(result) + return result.json() + + async def create_oauth2_client(self, body: dict, **kwargs) -> dict[str, Any]: + """Create a new OAuth2 client. + + Args: + body (dict): OAuth2 client details + + Returns: + OAuth2 client details as a dictionary. + """ + result = await self.c.post( + url=f"{self.v2api_host}/v2/oauth2clients/", + headers=self.config.get_headers(**kwargs), + json=body, + ) + handle_server_errors(result) + return result.json() + + async def update_oauth2_client( + self, client_id: str, body: dict, **kwargs + ) -> dict[str, Any]: + """Update an OAuth2 client by its client_id. + + Args: + client_id (str): OAuth2 client ID + body (dict): Update details + + Returns: + OAuth2 client details as a dictionary. + """ + result = await self.c.put( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/", + headers=self.config.get_headers(**kwargs), + json=body, + ) + handle_server_errors(result) + return result.json() + + async def update_oauth2_client_uris( + self, client_id: str, uris: dict, **kwargs + ) -> dict[str, Any]: + """Update OAuth2 client URIs. + + Args: + client_id (str): OAuth2 client ID + uris (dict): URIs update payload + + Returns: + OAuth2 client details as a dictionary. + """ + result = await self.c.patch( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/uris/", + headers=self.config.get_headers(**kwargs), + json=uris, + ) + handle_server_errors(result) + return result.json() + + async def delete_oauth2_client(self, client_id: str, **kwargs) -> None: + """Delete an OAuth2 client by its client_id. + + Args: + client_id (str): OAuth2 client ID + + Returns: + None if successful. + """ + result = await self.c.delete( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/", + headers=self.config.get_headers(**kwargs), + ) + handle_server_errors(result) + return None # -------------------Config Trees------------------- - @handle_and_munchify_response + async def list_configtrees( self, cont: int = 0, @@ -989,20 +1306,20 @@ async def list_configtrees( label_selector: list[str] = None, with_project: bool = True, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """List all config trees in a project. Args: cont (int, optional): Start index of config trees. Defaults to 0. limit (int, optional): Number of config trees to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get config trees from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get config trees from. Defaults to None. with_project (bool, optional): Include project details. Defaults to True. Returns: - Munch: List of config trees as a Munch object. + List of config trees as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/configtrees/", headers=self.config.get_headers(with_project=with_project, **kwargs), params={ @@ -1011,11 +1328,12 @@ async def list_configtrees( "labelSelector": label_selector, }, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response async def create_configtree( self, body: dict, with_project: bool = True, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Create a new config tree. Args: @@ -1023,16 +1341,17 @@ async def create_configtree( with_project (bool, optional): Work in the project scope. Defaults to True. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/configtrees/", headers=self.config.get_headers(with_project=with_project, **kwargs), json=body, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response async def get_configtree( self, name: str, @@ -1042,22 +1361,22 @@ async def get_configtree( revision: str = None, with_project: bool = True, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Get a config tree by its name. Args: name (str): Config tree name - content_types (list[str], optional): Define contentTypes to get config tree from. Defaults to None. + content_types (List[str], optional): Define contentTypes to get config tree from. Defaults to None. include_data (bool, optional): Include data. Defaults to False. - key_prefixes (list[str], optional): Define keyPrefixes to get config tree from. Defaults to None. + key_prefixes (List[str], optional): Define keyPrefixes to get config tree from. Defaults to None. revision (str, optional): Define revision to get config tree from. Defaults to None. with_project (bool, optional): Work in the project scope. Defaults to True. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(with_project=with_project, **kwargs), params={ @@ -1067,11 +1386,12 @@ async def get_configtree( "revision": revision, }, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response async def set_configtree_revision( self, name: str, configtree: object, project_guid: str = None, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Set a config tree revision. Args: @@ -1080,19 +1400,20 @@ async def set_configtree_revision( project_guid (str, optional): Project GUID. async defaults to None. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=configtree, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response async def update_configtree( self, name: str, body: dict, with_project: bool = True, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Update a config tree by its name. Args: @@ -1101,32 +1422,36 @@ async def update_configtree( with_project (bool, optional): Work in the project scope. Defaults to True. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(with_project=with_project, **kwargs), json=body, ) - @handle_and_munchify_response - async def delete_configtree(self, name: str, **kwargs) -> Munch: + handle_server_errors(result) + + return result.json() + + async def delete_configtree(self, name: str, **kwargs) -> None: """Delete a config tree by its name. Args: name (str): Config tree name Returns: - Munch: Config tree details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response async def list_revisions( self, tree_name: str, @@ -1135,7 +1460,7 @@ async def list_revisions( committed: bool = False, label_selector: list[str] = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """List all revisions of a config tree. Args: @@ -1143,27 +1468,32 @@ async def list_revisions( cont (int, optional): Continue param . Defaults to 0. limit (int, optional): Limit param . Defaults to 50. committed (bool, optional): Committed. Defaults to False. - label_selector (list[str], optional): Define labelSelector to get revisions from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get revisions from. Defaults to None. Returns: - Munch: List of revisions as a Munch object. + List of revisions as a dictionary. """ - return await self.c.get( + parameters = { + "continue": cont, + "limit": limit, + "committed": committed, + } + if label_selector: + parameters["labelSelector"] = label_selector + + result = await self.c.get( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/", headers=self.config.get_headers(**kwargs), - params={ - "continue": cont, - "limit": limit, - "committed": committed, - "labelSelector": label_selector, - }, + params=parameters, ) - @handle_and_munchify_response + handle_server_errors(result) + return result.json() + async def create_revision( self, name: str, body: dict, project_guid: str = None, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Create a new revision. Args: @@ -1172,19 +1502,21 @@ async def create_revision( project_guid (str): Project GUID (optional) Returns: - Munch: Revision details as a Munch object. + Revision details as a dictionary. """ - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/configtrees/{name}/revisions/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=body, ) - @handle_and_munchify_response + handle_server_errors(result) + return result.json() + async def put_keys_in_revision( self, name: str, revision_id: str, config_values: dict, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Put keys in a revision. Args: @@ -1193,16 +1525,18 @@ async def put_keys_in_revision( config_values (dict): Config values Returns: - Munch: Revision details as a Munch object. + Revision details as a dictionary. """ - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/configtrees/{name}/revisions/{revision_id}/keys/", headers=self.config.get_headers(**kwargs), json=config_values, ) - @handle_and_munchify_response + handle_server_errors(result) + return result.json() + async def commit_revision( self, tree_name: str, @@ -1211,7 +1545,7 @@ async def commit_revision( message: str = None, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Commit a revision. Args: @@ -1222,20 +1556,22 @@ async def commit_revision( project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Revision details as a Munch object. + Revision details as a dictionary. """ config_tree_revision = { "author": author, "message": message, } - return await self.c.patch( - url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/commit/", + result = await self.c.patch( + url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=config_tree_revision, ) - @handle_and_munchify_response + handle_server_errors(result) + return result.json() + async def get_key_in_revision( self, tree_name: str, @@ -1243,7 +1579,7 @@ async def get_key_in_revision( key: str, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Get a key in a revision. Args: @@ -1253,15 +1589,17 @@ async def get_key_in_revision( project_guid (str, optional): Project GUID. async defaults to None. Returns: - Munch: Key details as a Munch object. + Key details as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), ) - @handle_and_munchify_response + handle_server_errors(result) + return result.json() + async def put_key_in_revision( self, tree_name: str, @@ -1269,7 +1607,7 @@ async def put_key_in_revision( key: str, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Put a key in a revision. Args: @@ -1279,15 +1617,16 @@ async def put_key_in_revision( project_guid (str, optional): Project GUID. async defaults to None. Returns: - Munch: Key details as a Munch object. + Key details as a dictionary. """ - return await self.c.put( + result = await self.c.put( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response async def delete_key_in_revision( self, tree_name: str, @@ -1295,7 +1634,7 @@ async def delete_key_in_revision( key: str, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> None: """Delete a key in a revision. Args: @@ -1305,15 +1644,16 @@ async def delete_key_in_revision( project_guid (str, optional): Project GUID. async defaults to None. Returns: - Munch: Key details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response async def rename_key_in_revision( self, tree_name: str, @@ -1322,7 +1662,7 @@ async def rename_key_in_revision( config_key_rename: dict, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Rename a key in a revision. Args: @@ -1333,30 +1673,35 @@ async def rename_key_in_revision( project_guid (str, optional): Project GUID. async defaults to None. Returns: - Munch: Key details as a Munch object. + Key details as a dictionary. """ - return await self.c.patch( + result = await self.c.patch( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=config_key_rename, ) + handle_server_errors(result) + return result.json() + # Managed Service API - @handle_and_munchify_response - async def list_providers(self) -> Munch: + + async def list_providers(self) -> dict[str, Any]: """List all providers. Returns: - Munch: List of providers as a Munch object. + List of providers as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/managedservices/providers/", headers=self.config.get_headers(with_project=False), ) - @handle_and_munchify_response + handle_server_errors(result) + return ManagedServiceProviderList(**result.json()) + async def list_instances( self, cont: int = 0, @@ -1369,13 +1714,13 @@ async def list_instances( Args: cont (int, optional): Start index of instances. Defaults to 0. limit (int, optional): Number of instances to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get instances from. Defaults to None. - providers (list[str], optional): Define providers to get instances from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get instances from. Defaults to None. + providers (List[str], optional): Define providers to get instances from. Defaults to None. Returns: - Munch: List of instances as a Munch object. + List of instances as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/managedservices/", headers=self.config.get_headers(), params={ @@ -1386,50 +1731,61 @@ async def list_instances( }, ) - @handle_and_munchify_response - async def get_instance(self, name: str) -> Munch: + handle_server_errors(result) + return ManagedServiceInstanceList(**result.json()) + + async def get_instance(self, name: str) -> dict[str, Any]: """Get an instance by its name. Args: name (str): Instance name Returns: - Munch: Instance details as a Munch object. + Instance details as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/managedservices/{name}/", headers=self.config.get_headers(), ) - @handle_and_munchify_response - async def create_instance(self, body: dict) -> Munch: + handle_server_errors(result) + return ManagedServiceInstance(**result.json()) + + async def create_instance( + self, body: ManagedServiceInstance | dict + ) -> ManagedServiceInstance: """Create a new instance. Returns: - Munch: Instance details as a Munch object. + Instance details as a ManagedServiceInstance object. """ + if isinstance(body, dict): + body = ManagedServiceInstance.model_validate(body) - return await self.c.post( + result = await self.c.post( url=f"{self.v2api_host}/v2/managedservices/", headers=self.config.get_headers(), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - async def delete_instance(self, name: str) -> Munch: + handle_server_errors(result) + return ManagedServiceInstance(**result.json()) + + async def delete_instance(self, name: str) -> None: """Delete an instance. Returns: - Munch: Instance details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/managedservices/{name}/", headers=self.config.get_headers(), ) + handle_server_errors(result) + return None - @handle_and_munchify_response async def list_instance_bindings( self, instance_name: str, @@ -1443,12 +1799,12 @@ async def list_instance_bindings( instance_name (str): Instance name. cont (int, optional): Start index of instance bindings. Defaults to 0. limit (int, optional): Number of instance bindings to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get instance bindings from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get instance bindings from. Defaults to None. Returns: - Munch: List of instance bindings as a Munch object. + List of instance bindings as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/", headers=self.config.get_headers(), params={ @@ -1458,8 +1814,12 @@ async def list_instance_bindings( }, ) - @handle_and_munchify_response - async def create_instance_binding(self, instance_name: str, body: dict) -> Munch: + handle_server_errors(result) + return ManagedServiceBindingList(**result.json()) + + async def create_instance_binding( + self, instance_name: str, body: ManagedServiceBinding | dict + ) -> dict[str, Any]: """Create a new instance binding. Args: @@ -1467,17 +1827,22 @@ async def create_instance_binding(self, instance_name: str, body: dict) -> Munch body (object): Instance binding details. Returns: - Munch: Instance binding details as a Munch object. + Instance binding details as a dictionary. """ - return await self.c.post( + if isinstance(body, dict): + body = ManagedServiceBinding.model_validate(body) + + result = await self.c.post( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/", headers=self.config.get_headers(), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - async def get_instance_binding(self, instance_name: str, name: str) -> Munch: + handle_server_errors(result) + return ManagedServiceBinding(**result.json()) + + async def get_instance_binding(self, instance_name: str, name: str) -> dict[str, Any]: """Get an instance binding by its name. Args: @@ -1485,16 +1850,18 @@ async def get_instance_binding(self, instance_name: str, name: str) -> Munch: name (str): Instance binding name. Returns: - Munch: Instance binding details as a Munch object. + Instance binding details as a dictionary. """ - return await self.c.get( + result = await self.c.get( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/{name}/", headers=self.config.get_headers(), ) - @handle_and_munchify_response - async def delete_instance_binding(self, instance_name: str, name: str) -> Munch: + handle_server_errors(result) + return ManagedServiceBinding(**result.json()) + + async def delete_instance_binding(self, instance_name: str, name: str) -> None: """Delete an instance binding. Args: @@ -1502,10 +1869,12 @@ async def delete_instance_binding(self, instance_name: str, name: str) -> Munch: name (str): Instance binding name. Returns: - Munch: Instance binding details as a Munch object. + None if successful. """ - return await self.c.delete( + result = await self.c.delete( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/{name}/", headers=self.config.get_headers(), ) + handle_server_errors(result) + return None diff --git a/rapyuta_io_sdk_v2/client.py b/rapyuta_io_sdk_v2/client.py index 3c413ba..91a1aec 100644 --- a/rapyuta_io_sdk_v2/client.py +++ b/rapyuta_io_sdk_v2/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2024 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,15 +13,39 @@ # limitations under the License. import platform +from typing import Any import httpx -from munch import Munch from rapyuta_io_sdk_v2.config import Configuration -from rapyuta_io_sdk_v2.utils import handle_and_munchify_response, handle_server_errors - - -class Client(object): +from rapyuta_io_sdk_v2.models import ( + Secret, + StaticRoute, + Disk, + Deployment, + Package, + Project, + Network, + User, + ProjectList, + DeploymentList, + DiskList, + NetworkList, + PackageList, + SecretList, + StaticRouteList, + ManagedServiceBinding, + ManagedServiceBindingList, + ManagedServiceInstance, + ManagedServiceInstanceList, + ManagedServiceProviderList, + Organization, + Daemon, +) +from rapyuta_io_sdk_v2.utils import handle_server_errors + + +class Client: """Client class offers sync client for the v2 APIs. Args: @@ -42,12 +65,7 @@ def __init__(self, config: Configuration = None, **kwargs): ), headers={ "User-Agent": ( - "rio-sdk-v2;N/A;{};{};{} {}".format( - platform.processor() or platform.machine(), - platform.system(), - platform.release(), - platform.version(), - ) + f"rio-sdk-v2;N/A;{platform.processor() or platform.machine()};{platform.system()};{platform.release()} {platform.version()}" ) }, ) @@ -64,7 +82,7 @@ def get_auth_token(self, email: str, password: str) -> str: Returns: str: authentication token """ - response = self.c.post( + result = self.c.post( url=f"{self.rip_host}/user/login", headers={"Content-Type": "application/json"}, json={ @@ -72,8 +90,8 @@ def get_auth_token(self, email: str, password: str) -> str: "password": password, }, ) - handle_server_errors(response) - return response.json()["data"].get("token") + handle_server_errors(result) + return result.json()["data"].get("token") def login( self, @@ -93,8 +111,7 @@ def login( token = self.get_auth_token(email, password) self.config.auth_token = token - @handle_and_munchify_response - def logout(self, token: str = None) -> Munch: + def logout(self, token: str = None) -> dict[str, Any]: """Expire the authentication token. Args: @@ -104,13 +121,15 @@ def logout(self, token: str = None) -> Munch: if token is None: token = self.config.auth_token - return self.c.post( + result = self.c.post( url=f"{self.rip_host}/user/logout", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", }, ) + handle_server_errors(result) + return result.json() def refresh_token(self, token: str = None, set_token: bool = True) -> str: """Refresh the authentication token. @@ -126,15 +145,15 @@ def refresh_token(self, token: str = None, set_token: bool = True) -> str: if token is None: token = self.config.auth_token - response = self.c.post( + result = self.c.post( url=f"{self.rip_host}/refreshtoken", headers={"Content-Type": "application/json"}, json={"token": token}, ) - handle_server_errors(response) + handle_server_errors(result) if set_token: - self.config.auth_token = response.json()["data"].get("token") - return response.json()["data"].get("token") + self.config.auth_token = result.json()["data"].get("token") + return result.json()["data"].get("token") def set_organization(self, organization_guid: str) -> None: """Set the organization GUID. @@ -153,8 +172,7 @@ def set_project(self, project_guid: str) -> None: self.config.set_project(project_guid) # -----------------Organization---------------- - @handle_and_munchify_response - def get_organization(self, organization_guid: str = None, **kwargs) -> Munch: + def get_organization(self, organization_guid: str = None, **kwargs) -> Organization: """Get an organization by its GUID. If organization GUID is provided, the current organization GUID will be @@ -164,19 +182,21 @@ def get_organization(self, organization_guid: str = None, **kwargs) -> Munch: organization_guid (str): user provided organization GUID. Returns: - Munch: Organization details as a Munch object. + Organization: Organization details as an Organization object. """ - return self.c.get( + + result = self.c.get( url=f"{self.v2api_host}/v2/organizations/{organization_guid}/", headers=self.config.get_headers( with_project=False, organization_guid=organization_guid, **kwargs ), ) + handle_server_errors(result) + return Organization(**result.json()) - @handle_and_munchify_response def update_organization( - self, body: dict, organization_guid: str = None, **kwargs - ) -> Munch: + self, body: Organization | dict, organization_guid: str = None, **kwargs + ) -> Organization: """Update an organization by its GUID. Args: @@ -184,50 +204,60 @@ def update_organization( organization_guid (str, optional): Organization GUID. Defaults to None. Returns: - Munch: Organization details as a Munch object. + Organization: Organization details as an Organization object. """ - return self.c.put( + + if isinstance(body, dict): + body = Organization.model_validate(body) + + result = self.c.put( url=f"{self.v2api_host}/v2/organizations/{organization_guid}/", headers=self.config.get_headers( with_project=False, organization_guid=organization_guid, **kwargs ), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Organization(**result.json()) # ---------------------User-------------------- - @handle_and_munchify_response - def get_user(self, **kwargs) -> Munch: + def get_user(self, **kwargs) -> User: """Get User details. Returns: - Munch: User details as a Munch object. + User: User details as a User object. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/users/me/", headers=self.config.get_headers(with_project=False, **kwargs), ) + handle_server_errors(result) + return User(**result.json()) - @handle_and_munchify_response - def update_user(self, body: dict, **kwargs) -> Munch: + def update_user(self, body: User | dict, **kwargs) -> User: """Update the user details. Args: body (dict): User details Returns: - Munch: User details as a Munch object. + User: User details as a User object. """ - return self.c.put( + if isinstance(body, dict): + body = User.model_validate(body) + + result = self.c.put( url=f"{self.v2api_host}/v2/users/me/", headers=self.config.get_headers( with_project=False, with_organization=False, **kwargs ), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return User(**result.json()) # -------------------Project------------------- - @handle_and_munchify_response - def get_project(self, project_guid: str = None, **kwargs) -> Munch: + def get_project(self, project_guid: str = None, **kwargs) -> Project: """Get a project by its GUID. If no project or organization GUID is provided, @@ -241,7 +271,7 @@ def get_project(self, project_guid: str = None, **kwargs) -> Munch: ValueError: If organization_guid or project_guid is None Returns: - Munch: Project details as a Munch object. + Project: Project details as a Project object. """ if project_guid is None: project_guid = self.config.project_guid @@ -249,12 +279,13 @@ def get_project(self, project_guid: str = None, **kwargs) -> Munch: if not project_guid: raise ValueError("project_guid is required") - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/projects/{project_guid}/", headers=self.config.get_headers(with_project=False, **kwargs), ) + handle_server_errors(result) + return Project(**result.json()) - @handle_and_munchify_response def list_projects( self, cont: int = 0, @@ -262,52 +293,71 @@ def list_projects( label_selector: list[str] = None, status: list[str] = None, organizations: list[str] = None, + name: str = None, **kwargs, - ) -> Munch: + ) -> ProjectList: """List all projects in an organization. Args: cont (int, optional): Start index of projects. Defaults to 0. limit (int, optional): Number of projects to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get projects from. Defaults to None. - status (list[str], optional): Define status to get projects from. Defaults to None. - organizations (list[str], optional): Define organizations to get projects from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get projects from. Defaults to None. + status (List[str], optional): Define status to get projects from. Defaults to None. + organizations (List[str], optional): Define organizations to get projects from. Defaults to None. Returns: - Munch: List of projects as a Munch object. + Dict[str, Any]: List of projects with items validated as Project objects. """ - return self.c.get( + parameters = { + "continue": cont, + "limit": limit, + } + if organizations: + parameters["organizations"] = organizations + if label_selector: + parameters["labelSelector"] = label_selector + if status: + parameters["status"] = status + if name: + parameters["name"] = name + + result = self.c.get( url=f"{self.v2api_host}/v2/projects/", headers=self.config.get_headers(with_project=False, **kwargs), - params={ - "continue": cont, - "limit": limit, - "status": status, - "organizations": organizations, - "labelSelector": label_selector, - }, + params=parameters, ) - @handle_and_munchify_response - def create_project(self, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + return ProjectList(**result.json()) + + def create_project(self, body: Project | dict, **kwargs) -> Project: """Create a new project. Args: body (object): Project details Returns: - Munch: Project details as a Munch object. + Project: Project creation result. """ + if isinstance(body, dict): + body = Project.model_validate(body) + + org_guid = body.metadata.organizationGUID or None - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/projects/", - headers=self.config.get_headers(with_project=False, **kwargs), - json=body, + headers=self.config.get_headers( + organization_guid=org_guid, with_project=False, **kwargs + ), + json=body.model_dump(), ) + handle_server_errors(result) + return Project(**result.json()) - @handle_and_munchify_response - def update_project(self, body: dict, project_guid: str = None, **kwargs) -> Munch: + def update_project( + self, body: Project | dict, project_guid: str = None, **kwargs + ) -> Project: """Update a project by its GUID. Args: @@ -315,50 +365,55 @@ def update_project(self, body: dict, project_guid: str = None, **kwargs) -> Munc project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Project details as a Munch object. + Project: Project update result. """ + if isinstance(body, dict): + body = Project.model_validate(body) - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/projects/{project_guid}/", headers=self.config.get_headers(with_project=False, **kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Project(**result.json()) - @handle_and_munchify_response - def delete_project(self, project_guid: str, **kwargs) -> Munch: + def delete_project(self, project_guid: str, **kwargs) -> None: """Delete a project by its GUID. Args: project_guid (str): Project GUID Returns: - Munch: Project details as a Munch object. + None if successful. """ - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/projects/{project_guid}/", headers=self.config.get_headers(with_project=False, **kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response def update_project_owner( self, body: dict, project_guid: str = None, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Update the owner of a project by its GUID. Returns: - Munch: Project details as a Munch object. + Dict[str, Any]: Project owner update result. """ project_guid = project_guid or self.config.project_guid - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/projects/{project_guid}/owner/", headers=self.config.get_headers(**kwargs), json=body, ) + handle_server_errors(result) + return result # -------------------Package------------------- - @handle_and_munchify_response def list_packages( self, cont: int = 0, @@ -366,20 +421,20 @@ def list_packages( label_selector: list[str] = None, name: str = None, **kwargs, - ) -> Munch: + ) -> PackageList: """List all packages in a project. Args: cont (int, optional): Start index of packages. Defaults to 0. limit (int, optional): Number of packages to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get packages from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get packages from. Defaults to None. name (str, optional): Define name to get packages from. Defaults to None. Returns: - Munch: List of packages as a Munch object. + Dict[str, Any]: List of packages with items validated as Package objects. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/packages/", headers=self.config.get_headers(**kwargs), params={ @@ -390,25 +445,31 @@ def list_packages( }, ) - @handle_and_munchify_response - def create_package(self, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + return PackageList(**result.json()) + + def create_package(self, body: Package | dict, **kwargs) -> Package: """Create a new package. The Payload is the JSON format of the Package Manifest. For a documented example, run the rio explain package command. Returns: - Munch: Package details as a Munch object. + Package: Package details. """ + if isinstance(body, dict): + body = Package.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/packages/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - def get_package(self, name: str, version: str = None, **kwargs) -> Munch: + handle_server_errors(result) + return Package(**result.json()) + + def get_package(self, name: str, version: str = None, **kwargs) -> Package: """Get a package by its name. Args: @@ -416,33 +477,37 @@ def get_package(self, name: str, version: str = None, **kwargs) -> Munch: version (str, optional): Package version. Defaults to None. Returns: - Munch: Package details as a Munch object. + Package: Package details as a Package object. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/packages/{name}/", headers=self.config.get_headers(**kwargs), params={"version": version}, ) - @handle_and_munchify_response - def delete_package(self, name: str, **kwargs) -> Munch: + handle_server_errors(response=result) + return Package(**result.json()) + + def delete_package(self, name: str, version: str, **kwargs) -> None: """Delete a package by its name. Args: name (str): Package name Returns: - Munch: Package details as a Munch object. + None if successful. """ - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/packages/{name}/", headers=self.config.get_headers(**kwargs), + params={"version": version}, ) + handle_server_errors(result) + return None # -------------------Deployment------------------- - @handle_and_munchify_response def list_deployments( self, cont: int = 0, @@ -458,7 +523,7 @@ def list_deployments( phases: list[str] = None, regions: list[str] = None, **kwargs, - ) -> Munch: + ) -> DeploymentList: """List all deployments in a project. Args: @@ -466,20 +531,20 @@ def list_deployments( limit (int, optional): Number of deployments to list. Defaults to 50. dependencies (bool, optional): Filter by dependencies. Defaults to False. device_name (str, optional): Filter deployments by device name. Defaults to None. - guids (list[str], optional): Filter by GUIDs. Defaults to None. - label_selector (list[str], optional): Define labelSelector to get deployments from. Defaults to None. + guids (List[str], optional): Filter by GUIDs. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get deployments from. Defaults to None. name (str, optional): Define name to get deployments from. Defaults to None. - names (list[str], optional): Define names to get deployments from. Defaults to None. + names (List[str], optional): Define names to get deployments from. Defaults to None. package_name (str, optional): Filter by package name. Defaults to None. package_version (str, optional): Filter by package version. Defaults to None. - phases (list[str], optional): Filter by phases. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. - regions (list[str], optional): Filter by regions. Defaults to None. + phases (List[str], optional): Filter by phases. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. + regions (List[str], optional): Filter by regions. Defaults to None. Returns: - Munch: List of deployments as a Munch object. + Dict[str, Any]: List of deployments with items validated as Deployment objects. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/deployments/", headers=self.config.get_headers(**kwargs), params={ @@ -498,93 +563,123 @@ def list_deployments( }, ) - @handle_and_munchify_response - def create_deployment(self, body: dict, **kwargs) -> Munch: + handle_server_errors(response=result) + + return DeploymentList(**result.json()) + + def create_deployment(self, body: Deployment | dict, **kwargs) -> Deployment: """Create a new deployment. Args: body (object): Deployment details Returns: - Munch: Deployment details as a Munch object. + Deployment: Deployment details. """ + if isinstance(body, dict): + body = Deployment.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/deployments/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - def get_deployment(self, name: str, guid: str = None, **kwargs) -> Munch: + handle_server_errors(result) + return Deployment(**result.json()) + + def get_deployment(self, name: str, guid: str = None, **kwargs) -> Deployment: """Get a deployment by its name. Returns: - Munch: Deployment details as a Munch object. + Deployment details as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/deployments/{name}/", headers=self.config.get_headers(**kwargs), params={"guid": guid}, ) - @handle_and_munchify_response - def update_deployment(self, name: str, body: dict, **kwargs) -> Munch: + handle_server_errors(result) + return Deployment(**result.json()) + + def update_deployment( + self, name: str, body: Deployment | dict, **kwargs + ) -> Deployment: """Update a deployment by its name. Returns: - Munch: Deployment details as a Munch object. + Deployment: Deployment details. """ + if isinstance(body, dict): + body = Deployment.model_validate(body) - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/deployments/{name}/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Deployment(**result.json()) - @handle_and_munchify_response - def delete_deployment(self, name: str, **kwargs) -> Munch: + def delete_deployment(self, name: str, **kwargs) -> None: """Delete a deployment by its name. Returns: - Munch: Deployment details as a Munch object. + None if successful. """ - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/deployments/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response - def get_deployment_graph(self, name: str, **kwargs) -> Munch: + def get_deployment_graph(self, name: str, **kwargs) -> dict[str, Any]: """Get a deployment graph by its name. [Experimental] Returns: - Munch: Deployment graph as a Munch object. + Deployment graph as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/deployments/{name}/graph/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return result - @handle_and_munchify_response - def get_deployment_history(self, name: str, guid: str = None, **kwargs) -> Munch: + def get_deployment_history( + self, name: str, guid: str = None, **kwargs + ) -> dict[str, Any]: """Get a deployment history by its name. Returns: - Munch: Deployment history as a Munch object. + Deployment history as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/deployments/{name}/history/", headers=self.config.get_headers(**kwargs), params={"guid": guid}, ) + handle_server_errors(result) + return result + + def stream_deployment_logs(self, name: str, executable: str, replica: int = 0): + url = f"{self.v2api_host}/v2/deployments/{name}/logs/?replica={replica}&executable={executable}" + + with self.c.stream("GET", url=url, headers=self.config.get_headers()) as response: + # check status without reading the streaming content + response.raise_for_status() + + for line in response.iter_lines(): + if line: + yield line # -------------------Disks------------------- - @handle_and_munchify_response def list_disks( self, cont: int = 0, @@ -594,22 +689,22 @@ def list_disks( regions: list[str] = None, status: list[str] = None, **kwargs, - ) -> Munch: + ) -> DiskList: """List all disks in a project. Args: cont (int, optional): Start index of disks. Defaults to 0. - label_selector (list[str], optional): Define labelSelector to get disks from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get disks from. Defaults to None. limit (int, optional): Number of disks to list. Defaults to 50. - names (list[str], optional): Define names to get disks from. Defaults to None. - regions (list[str], optional): Define regions to get disks from. Defaults to None. - status (list[str], optional): Define status to get disks from. Available values : Available, Bound, Released, Failed, Pending.Defaults to None. + names (List[str], optional): Define names to get disks from. Defaults to None. + regions (List[str], optional): Define regions to get disks from. Defaults to None. + status (List[str], optional): Define status to get disks from. Available values : Available, Bound, Released, Failed, Pending.Defaults to None. Returns: - Munch: List of disks as a Munch object. + List of disks as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/disks/", headers=self.config.get_headers(**kwargs), params={ @@ -621,55 +716,81 @@ def list_disks( "status": status, }, ) + handle_server_errors(result) + return DiskList(**result.json()) - @handle_and_munchify_response - def get_disk(self, name: str, **kwargs) -> Munch: + def get_disk(self, name: str, **kwargs) -> Disk: """Get a disk by its name. Args: name (str): Disk name Returns: - Munch: Disk details as a Munch object. + Disk details as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/disks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return Disk(**result.json()) - @handle_and_munchify_response - def create_disk(self, body: str, **kwargs) -> Munch: + def create_disk(self, body: Disk | dict, **kwargs) -> Disk: """Create a new disk. Returns: - Munch: Disk details as a Munch object. + Disk: Disk details. """ + if isinstance(body, dict): + body = Disk.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/disks/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Disk(**result.json()) - @handle_and_munchify_response - def delete_disk(self, name: str, **kwargs) -> Munch: + def delete_disk(self, name: str, **kwargs) -> None: """Delete a disk by its name. Args: name (str): Disk name Returns: - Munch: Disk details as a Munch object. + None if successful. """ - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/disks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None + + # -------------------Device-------------------------- + + def get_device_daemons(self, device_guid: str): + """ + Retrieve the list of daemons associated with a specific device. + + Args: + device_guid (str): The unique identifier (GUID) of the device. + + Returns: + dict: The JSON response containing information about the device's daemons. + """ + result = self.c.get( + url=f"{self.v2api_host}/v2/devices/daemons/{device_guid}/", + headers=self.config.get_headers(), + ) + + handle_server_errors(response=result) + return Daemon(**result.json()) # -------------------Static Routes------------------- - @handle_and_munchify_response def list_staticroutes( self, cont: int = 0, @@ -679,22 +800,22 @@ def list_staticroutes( names: list[str] = None, regions: list[str] = None, **kwargs, - ) -> Munch: + ) -> StaticRouteList: """List all static routes in a project. Args: cont (int, optional): Start index of static routes. Defaults to 0. limit (int, optional): Number of static routes to list. Defaults to 50. - guids (list[str], optional): Define guids to get static routes from. Defaults to None. - label_selector (list[str], optional): Define labelSelector to get static routes from. Defaults to None. - names (list[str], optional): Define names to get static routes from. Defaults to None. - regions (list[str], optional): Define regions to get static routes from. Defaults to None. + guids (List[str], optional): Define guids to get static routes from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get static routes from. Defaults to None. + names (List[str], optional): Define names to get static routes from. Defaults to None. + regions (List[str], optional): Define regions to get static routes from. Defaults to None. Returns: - Munch: List of static routes as a Munch object. + List of static routes as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/staticroutes/", headers=self.config.get_headers(**kwargs), params={ @@ -706,39 +827,47 @@ def list_staticroutes( "regions": regions, }, ) + handle_server_errors(result) + return StaticRouteList(**result.json()) - @handle_and_munchify_response - def create_staticroute(self, body: dict, **kwargs) -> Munch: + def create_staticroute(self, body: StaticRoute | dict, **kwargs) -> StaticRoute: """Create a new static route. Returns: - Munch: Static route details as a Munch object. + StaticRoute: Static route details. """ + if isinstance(body, dict): + body = StaticRoute.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/staticroutes/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - def get_staticroute(self, name: str, **kwargs) -> Munch: + handle_server_errors(result) + return StaticRoute(**result.json()) + + def get_staticroute(self, name: str, **kwargs) -> StaticRoute: """Get a static route by its name. Args: name (str): Static route name Returns: - Munch: Static route details as a Munch object. + Static route details as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/staticroutes/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return StaticRoute(**result.json()) - @handle_and_munchify_response - def update_staticroute(self, name: str, body: dict, **kwargs) -> Munch: + def update_staticroute( + self, name: str, body: StaticRoute | dict, **kwargs + ) -> StaticRoute: """Update a static route by its name. Args: @@ -746,33 +875,38 @@ def update_staticroute(self, name: str, body: dict, **kwargs) -> Munch: body (dict): Update details Returns: - Munch: Static route details as a Munch object. + StaticRoute: Static route details. """ + if isinstance(body, dict): + body = StaticRoute.model_validate(body) - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/staticroutes/{name}/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - def delete_staticroute(self, name: str, **kwargs) -> Munch: + handle_server_errors(result) + return StaticRoute(**result.json()) + + def delete_staticroute(self, name: str, **kwargs) -> None: """Delete a static route by its name. Args: name (str): Static route name Returns: - Munch: Static route details as a Munch object. + None if successful. """ - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/staticroutes/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None # -------------------Networks------------------- - @handle_and_munchify_response def list_networks( self, cont: int = 0, @@ -785,25 +919,25 @@ def list_networks( regions: list[str] = None, status: list[str] = None, **kwargs, - ) -> Munch: + ) -> NetworkList: """List all networks in a project. Args: cont (int, optional): Start index of networks. Defaults to 0. limit (int, optional): Number of networks to list. Defaults to 50. device_name (str, optional): Filter networks by device name. Defaults to None. - label_selector (list[str], optional): Define labelSelector to get networks from. Defaults to None. - names (list[str], optional): Define names to get networks from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get networks from. Defaults to None. + names (List[str], optional): Define names to get networks from. Defaults to None. network_type (str, optional): Define network type to get networks from. Defaults to None. - phases (list[str], optional): Define phases to get networks from. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. - regions (list[str], optional): Define regions to get networks from. Defaults to None. - status (list[str], optional): Define status to get networks from. Available values : Running, Pending, Error, Unknown, Stopped. Defaults to None. + phases (List[str], optional): Define phases to get networks from. Available values : InProgress, Provisioning, Succeeded, FailedToUpdate, FailedToStart, Stopped. Defaults to None. + regions (List[str], optional): Define regions to get networks from. Defaults to None. + status (List[str], optional): Define status to get networks from. Available values : Running, Pending, Error, Unknown, Stopped. Defaults to None. Returns: - Munch: List of networks as a Munch object. + List of networks as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/networks/", headers=self.config.get_headers(**kwargs), params={ @@ -819,54 +953,62 @@ def list_networks( }, ) - @handle_and_munchify_response - def create_network(self, body: dict, **kwargs) -> Munch: + handle_server_errors(result) + return NetworkList(**result.json()) + + def create_network(self, body: Network | dict, **kwargs) -> Network: """Create a new network. Returns: - Munch: Network details as a Munch object. + Network: Network details. """ + if isinstance(body, dict): + body = Network.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/networks/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return Network(**result.json()) - @handle_and_munchify_response - def get_network(self, name: str, **kwargs) -> Munch: + def get_network(self, name: str, **kwargs) -> Network: """Get a network by its name. Args: name (str): Network name Returns: - Munch: Network details as a Munch object. + Network details as a Network class object. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/networks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return Network(**result.json()) - @handle_and_munchify_response - def delete_network(self, name: str, **kwargs) -> Munch: + def delete_network(self, name: str, **kwargs) -> None: """Delete a network by its name. Args: name (str): Network name Returns: - Munch: Network details as a Munch object. + None if successful. """ - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/networks/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None # -------------------Secrets------------------- - @handle_and_munchify_response + def list_secrets( self, cont: int = 0, @@ -875,64 +1017,76 @@ def list_secrets( names: list[str] = None, regions: list[str] = None, **kwargs, - ) -> Munch: + ) -> SecretList: """List all secrets in a project. Args: cont (int, optional): Start index of secrets. Defaults to 0. limit (int, optional): Number of secrets to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get secrets from. Defaults to None. - names (list[str], optional): Define names to get secrets from. Defaults to None. - regions (list[str], optional): Define regions to get secrets from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get secrets from. Defaults to None. + names (List[str], optional): Define names to get secrets from. Defaults to None. + regions (List[str], optional): Define regions to get secrets from. Defaults to None. Returns: - Munch: List of secrets as a Munch object. + List of secrets as a dictionary. """ - return self.c.get( + parameters = { + "continue": cont, + "limit": limit, + } + if label_selector is not None: + parameters["labelSelector"] = label_selector + if names is not None: + parameters["names"] = names + if regions is not None: + parameters["regions"] = regions + + result = self.c.get( url=f"{self.v2api_host}/v2/secrets/", headers=self.config.get_headers(**kwargs), - params={ - "continue": cont, - "limit": limit, - "labelSelector": label_selector, - "names": names, - "regions": regions, - }, + params=parameters, ) - @handle_and_munchify_response - def create_secret(self, body: dict, **kwargs) -> Munch: + handle_server_errors(result) + return SecretList(**result.json()) + + def create_secret(self, body: Secret | dict, **kwargs) -> Secret: """Create a new secret. Returns: - Munch: Secret details as a Munch object. + Secret: Secret details. """ + if isinstance(body, dict): + body = Secret.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/secrets/", - headers=self.config.get_headers(*kwargs), - json=body, + headers=self.config.get_headers(**kwargs), + json=body.model_dump(), ) - @handle_and_munchify_response - def get_secret(self, name: str, **kwargs) -> Munch: + handle_server_errors(result) + return Secret(**result.json()) + + def get_secret(self, name: str, **kwargs) -> Secret: """Get a secret by its name. Args: name (str): Secret name Returns: - Munch: Secret details as a Munch object. + Secret details as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/secrets/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(response=result) + return Secret(**result.json()) - @handle_and_munchify_response - def update_secret(self, name: str, body: dict, **kwargs) -> Munch: + def update_secret(self, name: str, body: Secret | dict, **kwargs) -> Secret: """Update a secret by its name. Args: @@ -940,33 +1094,169 @@ def update_secret(self, name: str, body: dict, **kwargs) -> Munch: body (dict): Update details Returns: - Munch: Secret details as a Munch object. + Secret: Secret details. """ + if isinstance(body, dict): + body = Secret.model_validate(body) - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/secrets/{name}/", headers=self.config.get_headers(**kwargs), - json=body, + json=body.model_dump(), ) - @handle_and_munchify_response - def delete_secret(self, name: str, **kwargs) -> Munch: + handle_server_errors(response=result) + return Secret(**result.json()) + + def delete_secret(self, name: str, **kwargs) -> None: """Delete a secret by its name. Args: name (str): Secret name Returns: - Munch: Secret details as a Munch object. + None if successful. """ - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/secrets/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None + + # -------------------OAuth2 Clients------------------- + def list_oauth2_clients( + self, + cont: int = 0, + limit: int = 50, + label_selector: list[str] = None, + names: list[str] = None, + regions: list[str] = None, + **kwargs, + ) -> dict[str, Any]: + """List all OAuth2 clients in a project. + + Args: + cont (int, optional): Start index. Defaults to 0. + limit (int, optional): Number to list. Defaults to 50. + label_selector (List[str], optional): Label selector. Defaults to None. + names (List[str], optional): Names filter. Defaults to None. + regions (List[str], optional): Regions filter. Defaults to None. + + Returns: + List of OAuth2 clients as a dictionary. + """ + params = { + "continue": cont, + "limit": limit, + } + if label_selector is not None: + params["labelSelector"] = label_selector + if names is not None: + params["names"] = names + if regions is not None: + params["regions"] = regions + + result = self.c.get( + url=f"{self.v2api_host}/v2/oauth2clients/", + headers=self.config.get_headers(**kwargs), + params=params, + ) + handle_server_errors(result) + return result.json() + + def get_oauth2_client(self, client_id: str, **kwargs) -> dict[str, Any]: + """Get an OAuth2 client by its client_id. + + Args: + client_id (str): OAuth2 client ID + + Returns: + OAuth2 client details as a dictionary. + """ + result = self.c.get( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/", + headers=self.config.get_headers(**kwargs), + ) + handle_server_errors(result) + return result.json() + + def create_oauth2_client(self, body: dict, **kwargs) -> dict[str, Any]: + """Create a new OAuth2 client. + + Args: + body (dict): OAuth2 client details + + Returns: + OAuth2 client details as a dictionary. + """ + result = self.c.post( + url=f"{self.v2api_host}/v2/oauth2clients/", + headers=self.config.get_headers(**kwargs), + json=body, + ) + handle_server_errors(result) + return result.json() + + def update_oauth2_client( + self, client_id: str, body: dict, **kwargs + ) -> dict[str, Any]: + """Update an OAuth2 client by its client_id. + + Args: + client_id (str): OAuth2 client ID + body (dict): Update details + + Returns: + OAuth2 client details as a dictionary. + """ + result = self.c.put( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/", + headers=self.config.get_headers(**kwargs), + json=body, + ) + handle_server_errors(result) + return result.json() + + def update_oauth2_client_uris( + self, client_id: str, uris: dict, **kwargs + ) -> dict[str, Any]: + """Update OAuth2 client URIs. + + Args: + client_id (str): OAuth2 client ID + uris (dict): URIs update payload + + Returns: + OAuth2 client details as a dictionary. + """ + result = self.c.patch( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/uris/", + headers=self.config.get_headers(**kwargs), + json=uris, + ) + handle_server_errors(result) + return result.json() + + def delete_oauth2_client(self, client_id: str, **kwargs) -> None: + """Delete an OAuth2 client by its client_id. + + Args: + client_id (str): OAuth2 client ID + + Returns: + None if successful. + """ + result = self.c.delete( + url=f"{self.v2api_host}/v2/oauth2clients/{client_id}/", + headers=self.config.get_headers(**kwargs), + ) + handle_server_errors(result) + return None # -------------------Config Trees------------------- - @handle_and_munchify_response + def list_configtrees( self, cont: int = 0, @@ -974,31 +1264,35 @@ def list_configtrees( label_selector: list[str] = None, with_project: bool = True, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """List all config trees in a project. Args: cont (int, optional): Start index of config trees. Defaults to 0. limit (int, optional): Number of config trees to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get config trees from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get config trees from. Defaults to None. with_project (bool, optional): Include project. Defaults to True. Returns: - Munch: List of config trees as a Munch object. + List of config trees as a dictionary. """ - - return self.c.get( + parameters = { + "continue": cont, + "limit": limit, + } + if label_selector: + parameters["labelSelector"] = label_selector + result = self.c.get( url=f"{self.v2api_host}/v2/configtrees/", headers=self.config.get_headers(with_project=with_project, **kwargs), - params={ - "continue": cont, - "limit": limit, - "labelSelector": label_selector, - }, + params=parameters, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response - def create_configtree(self, body: dict, with_project: bool = True, **kwargs) -> Munch: + def create_configtree( + self, body: dict, with_project: bool = True, **kwargs + ) -> dict[str, Any]: """Create a new config tree. Args: @@ -1006,16 +1300,16 @@ def create_configtree(self, body: dict, with_project: bool = True, **kwargs) -> with_project (bool, optional): Work in the project scope. Defaults to True. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/configtrees/", headers=self.config.get_headers(with_project=with_project, **kwargs), json=body, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def get_configtree( self, name: str, @@ -1025,22 +1319,21 @@ def get_configtree( revision: str = None, with_project: bool = True, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Get a config tree by its name. Args: name (str): Config tree name - content_types (list[str], optional): Define contentTypes to get config tree from. Defaults to None. + content_types (List[str], optional): Define contentTypes to get config tree from. Defaults to None. include_data (bool, optional): Include data. Defaults to False. - key_prefixes (list[str], optional): Define keyPrefixes to get config tree from. Defaults to None. + key_prefixes (List[str], optional): Define keyPrefixes to get config tree from. Defaults to None. revision (str, optional): Define revision to get config tree from. Defaults to None. with_project (bool, optional): Work in the project scope. Defaults to True. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(with_project=with_project, **kwargs), params={ @@ -1050,11 +1343,12 @@ def get_configtree( "revision": revision, }, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def set_configtree_revision( self, name: str, configtree: dict, project_guid: str = None, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Set a config tree revision. Args: @@ -1063,19 +1357,19 @@ def set_configtree_revision( project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=configtree, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def update_configtree( self, name: str, body: dict, with_project: bool = True, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Update a config tree by its name. Args: @@ -1084,32 +1378,32 @@ def update_configtree( with_project (bool, optional): Work in the project scope. Defaults to True. Returns: - Munch: Config tree details as a Munch object. + Config tree details as a dictionary. """ - - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(with_project=with_project, **kwargs), json=body, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response - def delete_configtree(self, name: str, **kwargs) -> Munch: + def delete_configtree(self, name: str, **kwargs) -> None: """Delete a config tree by its name. Args: name (str): Config tree name Returns: - Munch: Config tree details as a Munch object. + None if successful. """ - - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/configtrees/{name}/", headers=self.config.get_headers(**kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response def list_revisions( self, tree_name: str, @@ -1118,7 +1412,7 @@ def list_revisions( committed: bool = False, label_selector: list[str] = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """List all revisions of a config tree. Args: @@ -1126,27 +1420,29 @@ def list_revisions( cont (int, optional): Continue param . Defaults to 0. limit (int, optional): Limit param . Defaults to 50. committed (bool, optional): Committed. Defaults to False. - label_selector (list[str], optional): Define labelSelector to get revisions from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get revisions from. Defaults to None. Returns: - Munch: List of revisions as a Munch object. + List of revisions as a dictionary. """ - - return self.c.get( + parameters = { + "continue": cont, + "limit": limit, + "committed": committed, + } + if label_selector: + parameters["labelSelector"] = label_selector + result = self.c.get( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/", headers=self.config.get_headers(**kwargs), - params={ - "continue": cont, - "limit": limit, - "committed": committed, - "labelSelector": label_selector, - }, + params=parameters, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def create_revision( self, name: str, body: dict, project_guid: str = None, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Create a new revision. Args: @@ -1155,19 +1451,19 @@ def create_revision( project_guid (str): Project GUID (optional) Returns: - Munch: Revision details as a Munch object. + Revision details as a dictionary. """ - - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/configtrees/{name}/revisions/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=body, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def put_keys_in_revision( self, name: str, revision_id: str, config_values: dict, **kwargs - ) -> Munch: + ) -> dict[str, Any]: """Put keys in a revision. Args: @@ -1176,16 +1472,16 @@ def put_keys_in_revision( config_values (dict): Config values Returns: - Munch: Revision details as a Munch object. + Revision details as a dictionary. """ - - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/configtrees/{name}/revisions/{revision_id}/keys/", headers=self.config.get_headers(**kwargs), json=config_values, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def commit_revision( self, tree_name: str, @@ -1194,7 +1490,7 @@ def commit_revision( message: str = None, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Commit a revision. Args: @@ -1205,20 +1501,20 @@ def commit_revision( project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Revision details as a Munch object. + Revision details as a dictionary. """ config_tree_revision = { "author": author, "message": message, } - - return self.c.patch( - url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/commit/", + result = self.c.patch( + url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=config_tree_revision, ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def get_key_in_revision( self, tree_name: str, @@ -1226,7 +1522,7 @@ def get_key_in_revision( key: str, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Get a key in a revision. Args: @@ -1236,15 +1532,15 @@ def get_key_in_revision( project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Key details as a Munch object. + Key details as a dictionary. """ - - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def put_key_in_revision( self, tree_name: str, @@ -1252,7 +1548,7 @@ def put_key_in_revision( key: str, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Put a key in a revision. Args: @@ -1262,15 +1558,15 @@ def put_key_in_revision( project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Key details as a Munch object. + Key details as a dictionary. """ - - return self.c.put( + result = self.c.put( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), ) + handle_server_errors(result) + return result.json() - @handle_and_munchify_response def delete_key_in_revision( self, tree_name: str, @@ -1278,7 +1574,7 @@ def delete_key_in_revision( key: str, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> None: """Delete a key in a revision. Args: @@ -1288,15 +1584,15 @@ def delete_key_in_revision( project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Key details as a Munch object. + None if successful. """ - - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), ) + handle_server_errors(result) + return None - @handle_and_munchify_response def rename_key_in_revision( self, tree_name: str, @@ -1305,7 +1601,7 @@ def rename_key_in_revision( config_key_rename: dict, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Rename a key in a revision. Args: @@ -1316,49 +1612,50 @@ def rename_key_in_revision( project_guid (str, optional): Project GUID. Defaults to None. Returns: - Munch: Key details as a Munch object. + Key details as a dictionary. """ - - return self.c.patch( + result = self.c.patch( url=f"{self.v2api_host}/v2/configtrees/{tree_name}/revisions/{revision_id}/{key}/", headers=self.config.get_headers(project_guid=project_guid, **kwargs), json=config_key_rename, ) + handle_server_errors(result) + return result.json() # Managed Service API - @handle_and_munchify_response - def list_providers(self) -> Munch: + + def list_providers(self) -> ManagedServiceProviderList: """List all providers. Returns: - Munch: List of providers as a Munch object. + List of providers as a dictionary. """ - - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/managedservices/providers/", headers=self.config.get_headers(with_project=False), ) + handle_server_errors(result) + return ManagedServiceProviderList(**result.json()) - @handle_and_munchify_response def list_instances( self, cont: int = 0, limit: int = 50, label_selector: list[str] = None, providers: list[str] = None, - ): + ) -> ManagedServiceInstanceList: """List all instances in a project. Args: cont (int, optional): Start index of instances. Defaults to 0. limit (int, optional): Number of instances to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get instances from. Defaults to None. - providers (list[str], optional): Define providers to get instances from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get instances from. Defaults to None. + providers (List[str], optional): Define providers to get instances from. Defaults to None. Returns: - Munch: List of instances as a Munch object. + List of instances as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/managedservices/", headers=self.config.get_headers(), params={ @@ -1368,70 +1665,76 @@ def list_instances( "providers": providers, }, ) + handle_server_errors(result) + return ManagedServiceInstanceList(**result.json()) - @handle_and_munchify_response - def get_instance(self, name: str) -> Munch: + def get_instance(self, name: str) -> ManagedServiceInstance: """Get an instance by its name. Args: name (str): Instance name Returns: - Munch: Instance details as a Munch object. + Instance details as a dictionary. """ - - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/managedservices/{name}/", headers=self.config.get_headers(), ) + handle_server_errors(result) + return ManagedServiceInstance(**result.json()) - @handle_and_munchify_response - def create_instance(self, body: dict) -> Munch: + def create_instance( + self, body: ManagedServiceInstance | dict + ) -> ManagedServiceInstance: """Create a new instance. Returns: - Munch: Instance details as a Munch object. + Instance details as a ManagedServiceInstance object. """ + if isinstance(body, dict): + body = ManagedServiceInstance.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/managedservices/", headers=self.config.get_headers(), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return ManagedServiceInstance(**result.json()) - @handle_and_munchify_response - def delete_instance(self, name: str) -> Munch: + def delete_instance(self, name: str) -> None: """Delete an instance. Returns: - Munch: Instance details as a Munch object. + None if successful. """ - - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/managedservices/{name}/", headers=self.config.get_headers(), ) + handle_server_errors(result) + return None - @handle_and_munchify_response def list_instance_bindings( self, instance_name: str, cont: int = 0, limit: int = 50, label_selector: list[str] = None, - ): + ) -> ManagedServiceBindingList: """List all instance bindings in a project. Args: instance_name (str): Instance name. cont (int, optional): Start index of instance bindings. Defaults to 0. limit (int, optional): Number of instance bindings to list. Defaults to 50. - label_selector (list[str], optional): Define labelSelector to get instance bindings from. Defaults to None. + label_selector (List[str], optional): Define labelSelector to get instance bindings from. Defaults to None. Returns: - Munch: List of instance bindings as a Munch object. + List of instance bindings as a dictionary. """ - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/", headers=self.config.get_headers(), params={ @@ -1440,9 +1743,12 @@ def list_instance_bindings( "labelSelector": label_selector, }, ) + handle_server_errors(result) + return ManagedServiceBindingList(**result.json()) - @handle_and_munchify_response - def create_instance_binding(self, instance_name: str, body: dict) -> Munch: + def create_instance_binding( + self, instance_name: str, body: ManagedServiceBinding | dict + ) -> ManagedServiceBinding: """Create a new instance binding. Args: @@ -1450,17 +1756,22 @@ def create_instance_binding(self, instance_name: str, body: dict) -> Munch: body (object): Instance binding details. Returns: - Munch: Instance binding details as a Munch object. + Instance binding details as a dictionary. """ + if isinstance(body, dict): + body = ManagedServiceBinding.model_validate(body) - return self.c.post( + result = self.c.post( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/", headers=self.config.get_headers(), - json=body, + json=body.model_dump(), ) + handle_server_errors(result) + return ManagedServiceBinding(**result.json()) - @handle_and_munchify_response - def get_instance_binding(self, instance_name: str, name: str) -> Munch: + def get_instance_binding( + self, instance_name: str, name: str + ) -> ManagedServiceBinding: """Get an instance binding by its name. Args: @@ -1468,16 +1779,16 @@ def get_instance_binding(self, instance_name: str, name: str) -> Munch: name (str): Instance binding name. Returns: - Munch: Instance binding details as a Munch object. + Instance binding details as a dictionary. """ - - return self.c.get( + result = self.c.get( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/{name}/", headers=self.config.get_headers(), ) + handle_server_errors(result) + return ManagedServiceBinding(**result.json()) - @handle_and_munchify_response - def delete_instance_binding(self, instance_name: str, name: str) -> Munch: + def delete_instance_binding(self, instance_name: str, name: str) -> None: """Delete an instance binding. Args: @@ -1485,10 +1796,11 @@ def delete_instance_binding(self, instance_name: str, name: str) -> Munch: name (str): Instance binding name. Returns: - Munch: Instance binding details as a Munch object. + None if successful. """ - - return self.c.delete( + result = self.c.delete( url=f"{self.v2api_host}/v2/managedservices/{instance_name}/bindings/{name}/", headers=self.config.get_headers(), ) + handle_server_errors(result) + return None diff --git a/rapyuta_io_sdk_v2/config.py b/rapyuta_io_sdk_v2/config.py index 07ffe5c..b477cad 100644 --- a/rapyuta_io_sdk_v2/config.py +++ b/rapyuta_io_sdk_v2/config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2024 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,7 +25,7 @@ @dataclass -class Configuration(object): +class Configuration: """Configuration class for the SDK.""" email: str = None @@ -58,10 +57,10 @@ def from_file(cls, file_path: str = None) -> "Configuration": default_dir = get_default_app_dir(APP_NAME) file_path = os.path.join(default_dir, "config.json") - with open(file_path, "r") as file: + with open(file_path) as file: data = json.load(file) return cls( - email=data.get("email"), + email=data.get("email_id"), password=data.get("password"), project_guid=data.get("project_id"), organization_guid=data.get("organization_id"), diff --git a/rapyuta_io_sdk_v2/constants.py b/rapyuta_io_sdk_v2/constants.py index 89fcae0..afa6104 100644 --- a/rapyuta_io_sdk_v2/constants.py +++ b/rapyuta_io_sdk_v2/constants.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2024 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/rapyuta_io_sdk_v2/exceptions.py b/rapyuta_io_sdk_v2/exceptions.py index 3c233fa..d9f0ab0 100644 --- a/rapyuta_io_sdk_v2/exceptions.py +++ b/rapyuta_io_sdk_v2/exceptions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2024 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/rapyuta_io_sdk_v2/models/__init__.py b/rapyuta_io_sdk_v2/models/__init__.py new file mode 100644 index 0000000..c328758 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/__init__.py @@ -0,0 +1,18 @@ +from .secret import Secret as Secret, SecretList as SecretList +from .staticroute import StaticRoute as StaticRoute, StaticRouteList as StaticRouteList +from .disk import Disk as Disk, DiskList as DiskList +from .package import Package as Package, PackageList as PackageList +from .deployment import Deployment as Deployment, DeploymentList as DeploymentList +from .project import Project as Project, ProjectList as ProjectList +from .network import Network as Network, NetworkList as NetworkList +from .managedservice import ( + ManagedServiceProvider as ManagedServiceProvider, + ManagedServiceProviderList as ManagedServiceProviderList, + ManagedServiceInstance as ManagedServiceInstance, + ManagedServiceInstanceList as ManagedServiceInstanceList, + ManagedServiceBinding as ManagedServiceBinding, + ManagedServiceBindingList as ManagedServiceBindingList, +) +from .user import User as User +from .organization import Organization as Organization +from .daemons import Daemon as Daemon diff --git a/rapyuta_io_sdk_v2/models/daemons.py b/rapyuta_io_sdk_v2/models/daemons.py new file mode 100644 index 0000000..e713127 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/daemons.py @@ -0,0 +1,151 @@ +""" +Pydantic models for Daemon resource validation. + +This module contains Pydantic models that correspond to the Daemon JSON schema, +providing validation for Daemon resources to help users identify missing or +incorrect fields. +""" + +from typing import Literal +from pydantic import BaseModel, Field + +from rapyuta_io_sdk_v2.models.utils import BaseMetadata + + +# --- Daemon Status Types --- +DaemonStatusType = Literal["error", "running", "pending", "terminating", "terminated"] + + +# --- Configuration Models --- +class TracingConfig(BaseModel): + """Tracing configuration for daemons.""" + + enable: bool = Field(description="Enable tracing") + collector_endpoint: str | None = Field( + default=None, description="Collector endpoint for tracing" + ) + + +class AuthConfig(BaseModel): + """Authentication configuration for pull secrets.""" + + # This is a simplified AuthConfig for daemon pull secrets + # Add specific fields as needed based on actual requirements + username: str | None = Field(default=None, description="Username for authentication") + password: str | None = Field(default=None, description="Password for authentication") + registry: str | None = Field(default=None, description="Registry URL") + + +class VPNConfig(BaseModel): + """VPN configuration for daemons.""" + + enable: bool = Field(description="Enable VPN") + headscale_pre_auth_key: str | None = Field( + default=None, description="Headscale pre-authentication key" + ) + headscale_url: str | None = Field(default=None, description="Headscale URL") + headscale_acl_tag: str | None = Field(default=None, description="Headscale ACL tag") + advertise_routes: str | None = Field(default=None, description="Routes to advertise") + + +class TelegrafConfig(BaseModel): + """Telegraf configuration for daemons.""" + + enable: bool = Field(description="Enable Telegraf") + + +class DockerProxyConfig(BaseModel): + """Docker proxy configuration.""" + + registry: str | None = Field(default=None, description="Registry URL") + username: str | None = Field( + default=None, description="Username for proxy authentication" + ) + password: str | None = Field( + default=None, description="Password for proxy authentication" + ) + dataDirectory: str | None = Field(default=None, description="Data directory path") + + +class DockerMirrorConfig(BaseModel): + """Docker mirror configuration.""" + + url: str | None = Field(default=None, description="Mirror URL") + + +class DockerCacheConfig(BaseModel): + """Docker cache configuration for daemons.""" + + enable: bool = Field(description="Enable Docker cache") + proxy: DockerProxyConfig | None = Field( + default=None, description="Docker proxy configuration" + ) + mirror: DockerMirrorConfig | None = Field( + default=None, description="Docker mirror configuration" + ) + + +# --- Daemon Specification --- +class DaemonSpec(BaseModel): + """Specification for Daemon resource.""" + + tracing_config: TracingConfig | None = Field( + default=None, description="Tracing configuration" + ) + pull_secret: AuthConfig | None = Field( + default=None, description="Pull secret configuration" + ) + vpn_config: VPNConfig | None = Field(default=None, description="VPN configuration") + telegraf_config: TelegrafConfig | None = Field( + default=None, description="Telegraf configuration" + ) + docker_cache_config: DockerCacheConfig | None = Field( + default=None, description="Docker cache configuration" + ) + + +# --- Daemon Status --- +class DaemonStatus(BaseModel): + """Status information for a daemon.""" + + enable: bool = Field(description="Whether daemon is enabled or not") + status: DaemonStatusType | None = Field( + default=None, + description="Status of the daemon (pending, running, error, terminating, terminated)", + ) + error_code: str | None = Field( + default=None, description="Error code associated with the daemon" + ) + reason: str | None = Field( + default=None, description="Reason for the status of the daemon" + ) + restart_count: int = Field( + default=0, description="Number of times the daemon has been restarted" + ) + exit_code: int = Field( + default=0, description="Exit status code from the termination of the daemon" + ) + + +# --- Main Daemon Model --- +class Daemon(BaseModel): + """Daemon model.""" + + # TypeMeta fields (inline) + apiVersion: str | None = Field( + default="api.rapyuta.io/v2", + description="APIVersion defines the versioned schema of this representation of an object", + ) + kind: str = Field( + default="Daemon", + description="Kind is a string value representing the REST resource this object represents", + ) + + # ObjectMeta + metadata: BaseMetadata + + # Daemon-specific fields + spec: DaemonSpec = Field(default=None, description="Daemon specification") + status: dict[str, DaemonStatus | None] | None = Field( + default=None, description="Status of the daemon by component" + ) diff --git a/rapyuta_io_sdk_v2/models/deployment.py b/rapyuta_io_sdk_v2/models/deployment.py new file mode 100644 index 0000000..b64a9e2 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/deployment.py @@ -0,0 +1,260 @@ +""" +Pydantic models for Deployment resource validation. + +This module contains Pydantic models that correspond to the Deployment JSON schema, +providing validation for Deployment resources to help users identify missing or +incorrect fields. +""" + +from typing import Literal +from pydantic import BaseModel, Field, model_validator + +from rapyuta_io_sdk_v2.models.utils import ( + BaseMetadata, + BaseList, + Depends, + DeploymentPhase, + DeploymentStatusType, + ExecutableStatusType, + RestartPolicy, + Runtime, +) + + +class StringMap(dict[str, str]): + pass + + +# --- Depends Models --- +class PackageDepends(Depends): + kind: Literal["package"] = Field(default="package") + + +class DeploymentMetadata(BaseMetadata): + """Metadata for Deployment resource.""" + + depends: PackageDepends | None = None + generation: int | None = None + + +class EnvArgsSpec(BaseModel): + name: str + value: str | None = None + exposed: bool | None = None + exposed_name: str | None = Field(default=None, alias="exposedName") + + +class DeploymentVolume(BaseModel): + """Unified volume spec matching Go DeploymentVolume struct.""" + + execName: str | None = None + mountPath: str | None = None + subPath: str | None = None + uid: int | None = None + gid: int | None = None + perm: int | None = None + depends: Depends | None = None + + @model_validator(mode="before") + @classmethod + def handle_empty_depends(cls, data): + """Handle empty depends dictionaries by converting them to None.""" + if isinstance(data, dict) and "depends" in data: + depends = data["depends"] + # If depends is an empty dictionary, set it to None + if isinstance(depends, dict) and not depends: + data["depends"] = None + return data + + +class DeploymentStaticRoute(BaseModel): + """Static route configuration matching Go DeploymentStaticRoute struct.""" + + name: str | None = None + url: str | None = None + depends: Depends | None = None + + @model_validator(mode="before") + @classmethod + def handle_empty_depends(cls, data): + """Handle empty depends dictionaries by converting them to None.""" + if isinstance(data, dict) and "depends" in data: + depends = data["depends"] + # If depends is an empty dictionary, set it to None + if isinstance(depends, dict) and not depends: + data["depends"] = None + return data + + +class ManagedServiceSpec(BaseModel): + depends: dict[str, str] | None = None + + @model_validator(mode="before") + @classmethod + def handle_empty_depends(cls, data): + """Handle empty depends dictionaries by converting them to None.""" + if isinstance(data, dict) and "depends" in data: + depends = data["depends"] + # If depends is an empty dictionary, set it to None + if isinstance(depends, dict) and not depends: + data["depends"] = None + return data + + +class DeploymentROSNetwork(BaseModel): + """ROS Network configuration matching Go DeploymentROSNetwork struct.""" + + domainID: int | None = Field(default=None, description="ROS Domain ID") + depends: Depends | None = None + interface: str | None = Field(default=None, description="Network interface") + + @model_validator(mode="before") + @classmethod + def handle_empty_depends(cls, data): + """Handle empty depends dictionaries by converting them to None.""" + if isinstance(data, dict) and "depends" in data: + depends = data["depends"] + # If depends is an empty dictionary, set it to None + if isinstance(depends, dict) and not depends: + data["depends"] = None + return data + + +class DeploymentParamConfig(BaseModel): + """Param configuration matching Go DeploymentParamConfig struct.""" + + enabled: bool | None = None + trees: list[str] | None = None + blockUntilSynced: bool | None = Field(default=False) + + +class DeploymentVPNConfig(BaseModel): + """VPN configuration matching Go DeploymentVPNConfig struct.""" + + enabled: bool | None = Field(default=False) + + +class DeploymentFeatures(BaseModel): + """Features configuration matching Go DeploymentFeatures struct.""" + + params: DeploymentParamConfig | None = None + vpn: DeploymentVPNConfig | None = None + + +class DeploymentDevice(BaseModel): + """Device configuration matching Go DeploymentDevice struct.""" + + depends: Depends | None = None + + @model_validator(mode="before") + @classmethod + def handle_empty_depends(cls, data): + """Handle empty depends dictionaries by converting them to None.""" + if isinstance(data, dict) and "depends" in data: + depends = data["depends"] + # If depends is an empty dictionary, set it to None + if isinstance(depends, dict) and not depends: + data["depends"] = None + return data + + +class DeploymentSpec(BaseModel): + runtime: Runtime + depends: list[Depends] | None = None + device: DeploymentDevice | None = None + restart: RestartPolicy | None = None + envArgs: list[EnvArgsSpec] | None = None + volumes: list[DeploymentVolume] | None = None + rosNetworks: list[DeploymentROSNetwork] | None = None + features: DeploymentFeatures | None = None + staticRoutes: list[DeploymentStaticRoute] | None = None + managedServices: list[ManagedServiceSpec] | None = None + + @model_validator(mode="after") + def validate_runtime_and_volumes(self): + """Validate that runtime and volume configurations are compatible.""" + if self.runtime == "device" and self.volumes: + # For device runtime, volumes should not have cloud-specific depends + for volume in self.volumes: + if volume.depends and hasattr(volume.depends, "kind"): + # Device volumes should depend on disks, not cloud resources + if volume.depends.kind in ["managedService", "cloudService"]: + raise ValueError( + f"Device runtime cannot use cloud volume dependency: {volume.depends.kind}" + ) + elif self.runtime == "cloud" and self.volumes: + # For cloud runtime, volumes should not have device-specific fields + for volume in self.volumes: + if any( + [ + volume.uid is not None, + volume.gid is not None, + volume.perm is not None, + ] + ): + raise ValueError( + "Cloud runtime cannot use device-specific volume fields: uid, gid, perm" + ) + return self + + +class ExecutableStatus(BaseModel): + name: str | None = None + status: ExecutableStatusType | None = None + error_code: str | None = None + reason: str | None = None + restart_count: int | None = None + exit_code: int | None = None + + +class DependentDeploymentStatus(BaseModel): + name: str | None = None + guid: str | None = None + status: DeploymentStatusType | None = None + phase: DeploymentPhase | None = None + error_codes: list[str] | None = None + + +class DependentNetworkStatus(BaseModel): + name: str | None = None + guid: str | None = None + status: DeploymentStatusType | None = None + phase: DeploymentPhase | None = None + error_codes: list[str] | None = None + + +class DependentDiskStatus(BaseModel): + name: str | None = None + guid: str | None = None + status: str | None = None + error_codes: str | None = None + + +class Dependencies(BaseModel): + deployments: list[DependentDeploymentStatus] | None = None + networks: list[DependentNetworkStatus] | None = None + disks: list[DependentDiskStatus] | None = Field(default=None, alias="disk") + + +class DeploymentStatus(BaseModel): + phase: DeploymentPhase | None = None + status: DeploymentStatusType | None = None + error_codes: list[str] | None = None + executables_status: dict[str, ExecutableStatus] | None = None + dependencies: Dependencies | None = None + + +class Deployment(BaseModel): + """Deployment model.""" + + apiVersion: str | None = None + kind: str | None = None + metadata: DeploymentMetadata + spec: DeploymentSpec + status: DeploymentStatus | None = None + + +class DeploymentList(BaseList[Deployment]): + """List of deployments using BaseList.""" + + pass diff --git a/rapyuta_io_sdk_v2/models/disk.py b/rapyuta_io_sdk_v2/models/disk.py new file mode 100644 index 0000000..fd1657b --- /dev/null +++ b/rapyuta_io_sdk_v2/models/disk.py @@ -0,0 +1,76 @@ +""" +Pydantic models for Disk resource validation. + +This module contains Pydantic models that correspond to the Disk JSON schema, +providing validation for Disk resources to help users identify missing or +incorrect fields. +""" + +from typing import Literal +from pydantic import BaseModel, Field, field_validator + +from rapyuta_io_sdk_v2.models.utils import BaseMetadata, BaseList, Runtime + + +class DiskBound(BaseModel): + deployment_guid: str | None + deployment_name: str | None + + +class DiskSpec(BaseModel): + """Specification for Disk resource.""" + + runtime: Runtime = Field( + default="cloud", description="Runtime environment for the disk" + ) + capacity: int | float | None = Field(default=None, description="Disk capacity in GB") + + @field_validator("capacity") + @classmethod + def validate_capacity(cls, v): + """Validate disk capacity against allowed values.""" + if v is not None: + allowed_capacities = [4, 8, 16, 32, 64, 128, 256, 512] + if v not in allowed_capacities: + raise ValueError( + f"Disk capacity must be one of: {allowed_capacities}. Got: {v}" + ) + return v + + +class DiskStatus(BaseModel): + status: Literal["Available", "Bound", "Released", "Failed", "Pending"] + capacityUsed: float | None = Field( + default=None, description="Used disk capacity in GB" + ) + capacityAvailable: float | None = Field( + default=None, description="Available disk capacity in GB" + ) + errorCode: str | None = Field(default=None, description="Error code if any") + diskBound: DiskBound | None = Field( + default=None, description="Disk bound information" + ) + + @field_validator("diskBound", mode="before") + @classmethod + def normalize_disk_bound(cls, v): + """Convert empty dict to None for diskBound field.""" + if isinstance(v, dict) and not v: + return None + return v + + +class Disk(BaseModel): + """Disk model.""" + + apiVersion: str | None = Field(default="apiextensions.rapyuta.io/v1") + kind: str | None = Field(default="Disk") + metadata: BaseMetadata = Field(description="Metadata for the Disk resource") + spec: DiskSpec = Field(description="Specification for the Disk resource") + status: DiskStatus | None = Field(default=None) + + +class DiskList(BaseList[Disk]): + """List of disks using BaseList.""" + + pass diff --git a/rapyuta_io_sdk_v2/models/managedservice.py b/rapyuta_io_sdk_v2/models/managedservice.py new file mode 100644 index 0000000..a287be6 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/managedservice.py @@ -0,0 +1,136 @@ +"""Pydantic models for ManagedService resource.""" + +from typing import Any, Literal +from pydantic import BaseModel, Field + +from rapyuta_io_sdk_v2.models.utils import BaseMetadata, BaseList, ListMeta + + +ManagedServiceStatus = Literal["Pending", "Error", "Success", "Deleting", "Unknown"] + + +# --- ManagedServiceProvider Models --- + + +class ManagedServiceProvider(BaseModel): + """Managed service provider model.""" + + name: str | None = Field(default=None, description="Name of the provider") + + +class ManagedServiceProviderList(BaseModel): + """List of managed service providers.""" + + metadata: ListMeta | None = Field(default=None, description="List metadata") + items: list[ManagedServiceProvider] | None = Field( + default=[], description="List of providers" + ) + + +# --- ManagedServiceInstance Models --- + + +class ManagedServiceInstanceSpec(BaseModel): + """Specification for ManagedServiceInstance resource.""" + + provider: str | None = Field( + default=None, description="The provider for the managed service" + ) + config: Any = Field( + default=None, description="Configuration object for the managed service as JSON" + ) + + +class ManagedServiceInstanceStatus(BaseModel): + """Status for ManagedServiceInstance resource.""" + + status: ManagedServiceStatus | None = Field( + default=None, description="Current status of the managed service" + ) + error: str | None = Field( + default=None, description="Error message if any", alias="errorMessage" + ) + provider: Any = Field( + default=None, description="Provider-specific status information as JSON" + ) + + +class ManagedServiceInstance(BaseModel): + """Managed service instance model.""" + + apiVersion: str | None = Field(default=None, description="API version") + kind: str | None = Field(default=None, description="Resource kind") + metadata: BaseMetadata = Field(description="Resource metadata") + spec: ManagedServiceInstanceSpec | None = Field( + default=None, description="Instance specification" + ) + status: ManagedServiceInstanceStatus | None = Field( + default=None, description="Instance status" + ) + + +class ManagedServiceInstanceListOption(BaseList): + """List options for ManagedServiceInstance.""" + + providers: list[str] | None = Field(default=None, description="Filter by providers") + + +class ManagedServiceInstanceList(BaseList[ManagedServiceInstance]): + """List of managed service instances.""" + + pass + + +# --- ManagedServiceBinding Models --- + + +class ManagedServiceBindingSpec(BaseModel): + """Specification for ManagedServiceBinding resource.""" + + provider: str | None = Field( + default=None, description="The provider for the managed service" + ) + instance: str | None = Field( + default=None, description="The instance name/ID to bind to" + ) + environment: dict[str, str] | None = Field( + default=None, description="Environment variables" + ) + config: Any = Field(default=None, description="Configuration object as JSON") + throwaway: bool | None = Field( + default=None, description="Whether this is a throwaway binding" + ) + + +class ManagedServiceBindingStatus(BaseModel): + """Status for ManagedServiceBinding resource.""" + + # TODO: Update fields as needed + pass + + +class ManagedServiceBinding(BaseModel): + """Managed service binding model.""" + + apiVersion: str | None = Field(default=None, description="API version") + kind: str | None = Field(default=None, description="Resource kind") + metadata: BaseMetadata = Field(description="Resource metadata") + spec: ManagedServiceBindingSpec | None = Field( + default=None, description="Binding specification" + ) + status: ManagedServiceBindingStatus | None = Field( + default=None, description="Binding status" + ) + + +class ManagedServiceBindingListOption(BaseModel): + """List options for ManagedServiceBinding.""" + + # Add specific options as needed + pass + + +class ManagedServiceBindingList(BaseList[ManagedServiceBinding]): + """List of managed service bindings.""" + + pass diff --git a/rapyuta_io_sdk_v2/models/network.py b/rapyuta_io_sdk_v2/models/network.py new file mode 100644 index 0000000..2c2a1a4 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/network.py @@ -0,0 +1,82 @@ +""" +Pydantic models for Network resource validation. + +This module contains Pydantic models that correspond to the Network JSON schema, +providing validation for Network resources to help users identify missing or +incorrect fields. +""" + +from typing import Literal +from pydantic import AliasChoices, BaseModel, Field, field_validator + +from rapyuta_io_sdk_v2.models.utils import ( + Architecture, + BaseMetadata, + BaseList, + RestartPolicy, + Runtime, +) + + +class RabbitMQCreds(BaseModel): + defaultUser: str + defaultPassword: str + + +class ResourceLimits(BaseModel): + cpu: float = Field(..., multiple_of=0.025) + memory: int = Field(..., multiple_of=128) + + +class Depends(BaseModel): + kind: Literal["Device"] | None = Field(default="Device") + nameOrGuid: str = Field(validation_alias=AliasChoices("nameOrGuid", "nameOrGUID")) + + +class DiscoveryServerData(BaseModel): + serverID: int | None = None + serverPort: int | None = None + + +class NetworkSpec(BaseModel): + type: Literal["routed", "native"] + rosDistro: Literal["melodic", "kinetic", "noetic", "foxy"] + runtime: Runtime + discoveryServer: DiscoveryServerData | None = None + resourceLimits: ResourceLimits | None = None + depends: Depends | None = Field(default=None) + networkInterface: str | None = None + restartPolicy: RestartPolicy | None = None + architecture: Architecture | None = None + rabbitMQCreds: RabbitMQCreds | None = None + + # Needed as sometimes in result json depends comes as empty JSON + # For e.g., depends: {} + @field_validator("depends", mode="before") + @classmethod + def empty_dict_to_none(cls, v): + if v == {}: + return None + return v + + +class NetworkStatus(BaseModel): + phase: str + status: str + errorCodes: list[str] | None = None + + +class Network(BaseModel): + """Network model.""" + + apiVersion: str | None = None + kind: str | None = None + metadata: BaseMetadata | None = None + spec: NetworkSpec | None = None + status: NetworkStatus | None = None + + +class NetworkList(BaseList[Network]): + """List of networks using BaseList.""" + + pass diff --git a/rapyuta_io_sdk_v2/models/organization.py b/rapyuta_io_sdk_v2/models/organization.py new file mode 100644 index 0000000..fda9634 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/organization.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel, Field +from typing import Literal +from .utils import BaseMetadata + +# Define allowed roles +RoleType = Literal["admin", "viewer"] + + +class OrganizationUser(BaseModel): + guid: str = Field(default="", description="User GUID") + firstName: str = Field(default="", description="First name of the user") + lastName: str = Field(default="", description="Last name of the user") + emailID: str = Field(default="", description="Email ID of the user") + roleInOrganization: RoleType = Field(description="Role in the organization") + + +class OrganizationSpec(BaseModel): + users: list[OrganizationUser] = Field( + default_factory=list, description="List of users in the organization" + ) + + +class Organization(BaseModel): + metadata: BaseMetadata = Field(description="Metadata for the Organization resource") + spec: OrganizationSpec = Field( + description="Specification for the Organization resource" + ) diff --git a/rapyuta_io_sdk_v2/models/package.py b/rapyuta_io_sdk_v2/models/package.py new file mode 100644 index 0000000..03b51c4 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/package.py @@ -0,0 +1,165 @@ +""" +Pydantic models for Package resource validation. + +This module contains Pydantic models that correspond to the Package JSON schema, +providing validation for Package resources to help users identify missing or +incorrect fields. +""" + +from typing import Literal +from pydantic import BaseModel, Field, RootModel, field_validator, model_validator + +from rapyuta_io_sdk_v2.models.utils import ( + Architecture, + BaseMetadata, + BaseList, + RestartPolicy, + Runtime, +) + +# --- Helper Models --- + +EndpointProto = Literal[ + "external-http", + "external-https", + "external-tls-tcp", + "internal-tcp", + "internal-tcp-range", + "internal-udp", + "internal-udp-range", +] + + +class StringMap(dict[str, str]): + pass + + +class LivenessProbe(BaseModel): + httpGet: dict | None = None + exec: dict | None = None + tcpSocket: dict | None = None + initialDelaySeconds: int | None = Field(default=None, ge=1) + timeoutSeconds: int | None = Field(default=None, ge=10) + periodSeconds: int | None = Field(default=None, ge=1) + successThreshold: int | None = Field(default=None, ge=1) + failureThreshold: int | None = Field(default=None, ge=1) + + +class EnvironmentSpec(BaseModel): + name: str + description: str | None = None + default: str | None = None + exposed: bool | None = Field(default=None) + exposedName: str | None = None + + @field_validator("exposedName") + @classmethod + def validate_exposed_name(cls, v, info): + if info.data.get("exposed") and not v: + raise ValueError("exposedName is required when exposed is True") + return v + + +class CommandSpec(RootModel[list[str] | str | None]): + pass + + +class Limits(BaseModel): + cpu: float | None = Field(default=None, ge=0, le=256) + memory: float | int | None = Field(default=None, ge=0) + + +class DeviceDockerSpec(BaseModel): + image: str + imagePullPolicy: str | None = Field(default="IfNotPresent") + pullSecret: dict | None = None + + +class CloudDockerSpec(BaseModel): + image: str + pullSecret: dict | None = None + + +class Executable(BaseModel): + name: str | None = None + type: Literal["docker", "preInstalled"] = Field(default="docker") + docker: DeviceDockerSpec | None = None + command: list[str] | None = None + args: list[str] | None = None + limits: Limits | None = None + livenessProbe: LivenessProbe | None = None + uid: int | None = None + gid: int | None = None + + +class EndpointSpec(BaseModel): + name: str + type: EndpointProto | None = None + port: int | None = None + targetPort: int | None = None + portRange: str | None = None + + +class DeviceComponentInfoSpec(BaseModel): + arch: Architecture | None = Field(default="amd64") + restart: RestartPolicy | None = Field(default="always") + + +class CloudComponentInfoSpec(BaseModel): + replicas: int | None = Field(default=1) + + +class RosEndpointSpec(BaseModel): + type: str + name: str + compression: bool | None = Field(default=None) + scoped: bool | None = Field(default=None) + targeted: bool | None = Field(default=None) + qos: str | None = None + timeout: int | float | None = None + + +class RosComponentSpec(BaseModel): + enabled: bool | None = Field(default=False) + version: Literal["kinetic", "melodic", "noetic", "foxy"] | None = None + rosEndpoints: list[RosEndpointSpec] | None = None + + +class PackageSpec(BaseModel): + runtime: Runtime | None = None + executables: list[Executable] | None = None + environmentVars: list[EnvironmentSpec] | None = None + ros: RosComponentSpec | None = None + endpoints: list[EndpointSpec] | None = None + device: DeviceComponentInfoSpec | None = None + cloud: CloudComponentInfoSpec | None = None + hostPID: bool | None = None + + @model_validator(mode="after") + @staticmethod + def check_spec_device_or_cloud(obj): + if obj.runtime == "device" and obj.cloud is not None: + raise ValueError("'cloud' section must not be set when runtime is 'device'.") + if obj.runtime == "cloud" and obj.device is not None: + raise ValueError("'device' section must not be set when runtime is 'cloud'.") + return obj + + +class PackageMetadata(BaseMetadata): + version: str | None + description: str | None = Field(default=None) + + +class Package(BaseModel): + """Package model.""" + + apiVersion: str | None = Field(default="api.rapyuta.io/v2") + kind: Literal["Package"] = Field(default="Package") + metadata: PackageMetadata + spec: PackageSpec + + +class PackageList(BaseList[Package]): + """List of packages using BaseList.""" + + pass diff --git a/rapyuta_io_sdk_v2/models/project.py b/rapyuta_io_sdk_v2/models/project.py new file mode 100644 index 0000000..430923c --- /dev/null +++ b/rapyuta_io_sdk_v2/models/project.py @@ -0,0 +1,97 @@ +""" +Pydantic models for Project resource validation. + +This module contains Pydantic models that correspond to the Project JSON schema, +providing validation for Project resources to help users identify missing or +incorrect fields. +""" + +from typing import Literal +from pydantic import BaseModel, Field, model_validator + +from rapyuta_io_sdk_v2.models.utils import BaseMetadata, BaseList + + +class RoleSpec(str): + pass + + +class User(BaseModel): + emailID: str + firstName: str | None = None + lastName: str | None = None + userGUID: str | None = None + role: Literal["admin", "viewer"] | None = Field(default="viewer") + + +class UserGroup(BaseModel): + name: str + userGroupGUID: str | None = None + role: Literal["admin", "viewer"] | None = Field(default="viewer") + + +class FeaturesVPN(BaseModel): + subnets: list[str] | None = None + enabled: bool = Field(default=False) + + +class FeaturesTracing(BaseModel): + enabled: bool = Field(default=False) + + +class FeaturesDockerCache(BaseModel): + enabled: bool = Field(default=False) + proxyDevice: str | None = None + proxyInterface: str | None = None + registrySecret: str | None = None + registryURL: str | None = None + dataDirectory: str | None = Field(default="/opt/rapyuta/volumes/docker-cache/") + + @model_validator(mode="after") + def validate_data_directory(self): + if not self.enabled: + self.dataDirectory = None + return self + + +class Features(BaseModel): + vpn: FeaturesVPN | None = None + tracing: FeaturesTracing | None = None + dockerCache: FeaturesDockerCache | None = None + + +class ProjectSpec(BaseModel): + users: list[User] | None = None + userGroups: list[UserGroup] | None = None + features: Features | None = None + + +class ProjectStatus(BaseModel): + status: str | None = None + vpn: str | None = None + tracing: str | None = None + + +class Metadata(BaseMetadata): + """Metadata for Project resource.""" + + pass + + +class Project(BaseModel): + """Project model.""" + + apiVersion: str | None = None + kind: str | None = None + metadata: BaseMetadata | None = None + spec: ProjectSpec | None = None + status: ProjectStatus | None = None + + +class ProjectList(BaseList[Project]): + """List of projects using BaseList.""" + + pass + """List of Project resources.""" + + pass diff --git a/rapyuta_io_sdk_v2/models/secret.py b/rapyuta_io_sdk_v2/models/secret.py new file mode 100644 index 0000000..89d7ea4 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/secret.py @@ -0,0 +1,57 @@ +""" +Pydantic models for Secret resource validation. + +This module contains Pydantic models that correspond to the Secret JSON schema, +providing validation for Secret resources to help users identify missing or +incorrect fields. +""" + +from pydantic import BaseModel, Field, field_validator + +from rapyuta_io_sdk_v2.models.utils import BaseMetadata, BaseList + + +class DockerSpec(BaseModel): + """Docker registry configuration for secrets.""" + + registry: str = Field( + default="https://index.docker.io/v1/", description="Docker registry URL" + ) + username: str = Field(description="Username for docker registry authentication") + password: str | None = Field( + default=None, description="Password for docker registry authentication" + ) + email: str = Field(description="Email for docker registry authentication") + + @field_validator("registry", "username", "password", "email", mode="after") + @classmethod + def not_empty(cls, v, info): + # Only require password if it's not None + if info.field_name == "password" and v is None: + return v + if not v or (isinstance(v, str) and v.strip() == ""): + raise ValueError(f"{info.field_name} is required and cannot be empty") + return v + + +class SecretSpec(BaseModel): + """Specification for Secret resource.""" + + docker: DockerSpec | None = Field( + default=None, description="Docker registry configuration when type is Docker" + ) + + +class Secret(BaseModel): + """Secret model.""" + + apiVersion: str | None = None + kind: str | None = None + metadata: BaseMetadata | None = None + spec: SecretSpec = Field(description="Specification for the Secret resource") + + +class SecretList(BaseList[Secret]): + """List of secrets using BaseList.""" + + pass diff --git a/rapyuta_io_sdk_v2/models/staticroute.py b/rapyuta_io_sdk_v2/models/staticroute.py new file mode 100644 index 0000000..7eff4fd --- /dev/null +++ b/rapyuta_io_sdk_v2/models/staticroute.py @@ -0,0 +1,81 @@ +""" +Pydantic models for StaticRoute resource validation. + +This module contains Pydantic models that correspond to the StaticRoute JSON schema, +providing validation for StaticRoute resources to help users identify missing or +incorrect fields. +""" + +from typing import Literal +from pydantic import BaseModel, Field, field_validator + +from .utils import BaseList, BaseMetadata +import re + + +class StaticRouteSpec(BaseModel): + """Specification for StaticRoute resource.""" + + url: str | None = Field(default=None, description="URL for the static route") + sourceIPRange: list[str] | None = Field( + default=None, description="List of source IP ranges in CIDR notation" + ) + + @field_validator("sourceIPRange") + @classmethod + def validate_ip_ranges(cls, v): + """Validate IP range format (CIDR notation).""" + if v is not None: + ip_pattern = ( + r"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(?:/([1-9]|1\d|2\d|3[0-2]))?$" + ) + for ip_range in v: + if not re.match(ip_pattern, ip_range): + raise ValueError( + f"Invalid IP range format: {ip_range}. " + "Must be a valid CIDR notation (e.g., 192.168.1.0/24)" + ) + return v + + +class StaticRouteStatus(BaseModel): + """Status for StaticRoute resource.""" + + status: Literal["Available", "Unavailable"] | None = Field( + default=None, description="Status of the static route" + ) + packageID: str | None = Field( + default=None, description="Package ID associated with the static route" + ) + deploymentID: str | None = Field( + default=None, description="Deployment ID associated with the static route" + ) + + +class StaticRoute(BaseModel): + """ + StaticRoute resource model for validation. + + This model validates StaticRoute resources according to the JSON schema, + helping users identify missing or incorrect configuration. + A named route for the Deployment endpoint. + """ + + apiVersion: Literal["apiextensions.rapyuta.io/v1", "api.rapyuta.io/v2"] = Field( + default="api.rapyuta.io/v2", + description="API version for the StaticRoute resource", + ) + kind: Literal["StaticRoute"] = Field( + description="Resource kind, must be 'StaticRoute'" + ) + metadata: BaseMetadata = Field(description="Metadata for the StaticRoute resource") + spec: StaticRouteSpec | None = Field( + default=None, description="Specification for the StaticRoute resource" + ) + status: StaticRouteStatus | None = Field( + default=None, description="Status of the StaticRoute resource" + ) + + +class StaticRouteList(BaseList[StaticRoute]): + pass diff --git a/rapyuta_io_sdk_v2/models/user.py b/rapyuta_io_sdk_v2/models/user.py new file mode 100644 index 0000000..0213a5e --- /dev/null +++ b/rapyuta_io_sdk_v2/models/user.py @@ -0,0 +1,74 @@ +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel, Field +from rapyuta_io_sdk_v2.models.utils import BaseMetadata, BaseList + + +class UserOrganization(BaseModel): + """User organization model.""" + + creator: str | None = None + guid: str | None = None + name: str | None = None + shortGUID: str | None = Field(None, alias="shortGUID") + + +class UserProject(BaseModel): + """User project model.""" + + creator: str | None = None + guid: str | None = None + name: str | None = None + organizationCreatorGUID: str | None = None + organizationGUID: str | None = None + + +class UserGroup(BaseModel): + """User group model.""" + + creator: str | None = None + guid: str | None = None + name: str | None = None + organizationCreatorGUID: str | None = None + organizationGUID: str | None = None + + +class UserSpec(BaseModel): + """User specification model.""" + + emailID: str | None = None + firstName: str | None = None + lastName: str | None = None + organizations: list[UserOrganization] | None = None + password: str | None = None + projects: list[UserProject] | None = None + userGroupAdmins: list[UserGroup] | None = None + userGroupsMembers: list[UserGroup] | None = None + + +class User(BaseModel): + """User model.""" + + apiVersion: str | None = None + kind: str | None = None + metadata: BaseMetadata | None = None + spec: UserSpec | None = None + + +class UserList(BaseList[User]): + """List of users using BaseList.""" + + pass + pass diff --git a/rapyuta_io_sdk_v2/models/utils.py b/rapyuta_io_sdk_v2/models/utils.py new file mode 100644 index 0000000..00f2c3a --- /dev/null +++ b/rapyuta_io_sdk_v2/models/utils.py @@ -0,0 +1,113 @@ +from pydantic import BaseModel, Field + + +from typing import Generic, Literal, TypeVar + + +# Type variable for generic list items +T = TypeVar("T") + + +class BaseMetadata(BaseModel): + """Base metadata class containing common fields across all resource types. + + Based on server ObjectMeta struct that holds all the meta information + related to a resource such as name, timestamps, etc. + """ + + # Name of the resource + name: str = Field(description="Name of the resource") + + # GUID is a globally unique identifier on Rapyuta.io platform + guid: str | None = Field(default=None, description="GUID of the resource") + + # Project and Organization information + projectGUID: str | None = Field(default=None, description="Project GUID") + organizationGUID: str | None = Field(default=None, description="Organization GUID") + organizationCreatorGUID: str | None = Field( + default=None, description="Organization creator GUID" + ) + + # Creator information + creatorGUID: str | None = Field(default=None, description="Creator GUID") + + # Labels are key-value pairs associated with the resource + labels: dict[str, str] | None = Field( + default=None, description="Labels as key-value pairs" + ) + + # Region information + region: str | None = Field(default=None, description="Region") + + # Timestamps + createdAt: str | None = Field(default=None, description="Time of resource creation") + updatedAt: str | None = Field(default=None, description="Time of resource update") + deletedAt: str | None = Field(default=None, description="Time of resource deletion") + + # Human-readable names + organizationName: str | None = Field(default=None, description="Organization name") + shortGUID: str | None = Field(default=None, description="Short GUID") + projectName: str | None = Field(default=None, description="Project name") + + +class ListMeta(BaseModel): + """Metadata for list responses based on Kubernetes ListMeta.""" + + continue_: int | None = Field( + default=None, + alias="continue", + description="Continue token for pagination (int64)", + ) + + +class BaseList(BaseModel, Generic[T]): + """Base list class for validating list method results. + + Corresponds to Go struct: + type ProjectList struct { + metav1.TypeMeta `json:",inline,omitempty"` + ListMeta `json:"metadata,omitempty"` + Items []Project `json:"items,omitempty"` + } + """ + + # TypeMeta fields (inline) + kind: str | None = Field( + default=None, + description="Kind is a string value representing the REST resource this object represents", + ) + apiVersion: str | None = Field( + default="api.rapyuta.io/v2", + description="APIVersion defines the versioned schema of this representation of an object", + ) + + # ListMeta + metadata: ListMeta | None = Field(default=None, description="List metadata") + + # Items + items: list[T] | None = Field(default=[], description="List of resource items") + + +class Depends(BaseModel): + kind: str + nameOrGUID: str # Keep for backward compatibility, but add GUID field + wait: bool | None = None + version: str | None = None + + +RestartPolicy = Literal["always", "never", "onfailure"] +Runtime = Literal["device", "cloud"] +ExecutableStatusType = Literal[ + "error", "running", "pending", "terminating", "terminated", "unknown" +] +DeploymentStatusType = Literal["Running", "Pending", "Error", "Unknown", "Stopped"] +# --- Constants matching Go server-side --- +DeploymentPhase = Literal[ + "InProgress", + "Provisioning", + "Succeeded", + "FailedToUpdate", + "FailedToStart", + "Stopped", +] +Architecture = Literal["amd64", "arm32v7", "arm64v8"] diff --git a/rapyuta_io_sdk_v2/utils.py b/rapyuta_io_sdk_v2/utils.py index e16fcd9..d9f032f 100644 --- a/rapyuta_io_sdk_v2/utils.py +++ b/rapyuta_io_sdk_v2/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2024 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,15 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # from rapyuta_io_sdk_v2.config import Configuration -import asyncio import json import os import sys import typing -from functools import wraps import httpx -from munch import Munch, munchify import rapyuta_io_sdk_v2.exceptions as exceptions @@ -38,6 +34,11 @@ def handle_server_errors(response: httpx.Response): except json.JSONDecodeError: err = response.text + # 400 error + if status_code == httpx.codes.BAD_REQUEST: + raise exceptions.MethodNotAllowedError(err) + if status_code == httpx.codes.FORBIDDEN: + raise exceptions.MethodNotAllowedError(err) # 404 Not Found if status_code == httpx.codes.NOT_FOUND: raise exceptions.HttpNotFoundError(err) @@ -67,7 +68,7 @@ def handle_server_errors(response: httpx.Response): raise exceptions.UnauthorizedAccessError(err) # Anything else that is not known - if status_code > 504: + if status_code > 400: raise exceptions.UnknownError(err) @@ -90,39 +91,13 @@ def get_default_app_dir(app_name: str) -> str: return os.path.join(xdg_config_home, app_name) -# Decorator to handle server errors and munchify response -def handle_and_munchify_response(func) -> typing.Callable: - """Decorator to handle server errors and munchify response. - - Args: - func (callable): The function to decorate. - """ - - @wraps(func) - async def async_wrapper(*args, **kwargs) -> Munch: - response = await func(*args, **kwargs) - handle_server_errors(response) - return munchify(response.json()) - - @wraps(func) - def sync_wrapper(*args, **kwargs) -> Munch: - response = func(*args, **kwargs) - handle_server_errors(response) - return munchify(response.json()) - - if asyncio.iscoroutinefunction(func): - return async_wrapper - - return sync_wrapper - - def walk_pages( func: typing.Callable, *args, limit: int = 50, cont: int = 0, **kwargs, -) -> typing.Generator: +): """A generator function to paginate through list API results. Args: @@ -133,20 +108,19 @@ def walk_pages( **kwargs: Additional keyword arguments to pass to the API function. Yields: - Munch: Each item from the API response. + Dict[str, Any]: Each item from the API response. """ while True: data = func(cont, limit, *args, **kwargs) - items = data.get("items", []) + items = data.items or [] if not items: break - for item in items: - yield munchify(item) + yield items # Update `cont` for the next page - cont = data.get("metadata", {}).get("continue") + cont = data.metadata.continue_ or None if cont is None: break @@ -168,19 +142,18 @@ async def walk_pages_async( **kwargs: Additional keyword arguments to pass to the API function. Yields: - Munch: Each item from the API response. + Dict[str, Any]: Each item from the API response. """ while True: data = await func(cont, limit, *args, **kwargs) - items = data.get("items", []) + items = data.items or [] if not items: break - for item in items: - yield munchify(item) + yield items # Update `cont` for the next page - cont = data.get("metadata", {}).get("continue") + cont = data.metadata.continue_ or None if cont is None: break diff --git a/tests/async_tests/test_configtree_async.py b/tests/async_tests/test_configtree_async.py index 8976496..e6ad3d3 100644 --- a/tests/async_tests/test_configtree_async.py +++ b/tests/async_tests/test_configtree_async.py @@ -1,15 +1,15 @@ import httpx import pytest -import pytest_asyncio # noqa: F401 -from munch import Munch +import pytest_asyncio from asyncmock import AsyncMock -from tests.data.mock_data import configtree_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from tests.data.mock_data import configtree_body +from tests.utils.fixtures import async_client @pytest.mark.asyncio -async def test_list_configtrees_success(client, mocker: AsyncMock): # noqa: F811 +async def test_list_configtrees_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") @@ -23,17 +23,16 @@ async def test_list_configtrees_success(client, mocker: AsyncMock): # noqa: F81 ) # Call the list_configtrees method - response = await client.list_configtrees() + response = await async_client.list_configtrees() # Validate the response - assert isinstance(response, Munch) assert response["items"] == [ {"name": "test-configtree", "guid": "mock_configtree_guid"} ] @pytest.mark.asyncio -async def test_list_configtrees_bad_gateway(client, mocker: AsyncMock): # noqa: F811 +async def test_list_configtrees_bad_gateway(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") @@ -45,13 +44,13 @@ async def test_list_configtrees_bad_gateway(client, mocker: AsyncMock): # noqa: # Call the list_configtrees method with pytest.raises(Exception) as exc: - await client.list_configtrees() + await async_client.list_configtrees() assert str(exc.value) == "bad gateway" @pytest.mark.asyncio -async def test_create_configtree_success(client, mocker: AsyncMock): # noqa: F811 +async def test_create_configtree_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.post method mock_post = mocker.patch("httpx.AsyncClient.post") @@ -64,15 +63,14 @@ async def test_create_configtree_success(client, mocker: AsyncMock): # noqa: F8 ) # Call the create_configtree method - response = await client.create_configtree(configtree_body) + response = await async_client.create_configtree(configtree_body) # Validate the response - assert isinstance(response, Munch) assert response["metadata"]["guid"] == "test_configtree_guid" @pytest.mark.asyncio -async def test_create_configtree_service_unavailable(client, mocker: AsyncMock): # noqa: F811 +async def test_create_configtree_service_unavailable(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.post method mock_post = mocker.patch("httpx.AsyncClient.post") @@ -84,13 +82,13 @@ async def test_create_configtree_service_unavailable(client, mocker: AsyncMock): # Call the create_configtree method with pytest.raises(Exception) as exc: - await client.create_configtree(configtree_body) + await async_client.create_configtree(configtree_body) assert str(exc.value) == "service unavailable" @pytest.mark.asyncio -async def test_get_configtree_success(client, mocker: AsyncMock): # noqa: F811 +async def test_get_configtree_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") @@ -103,16 +101,15 @@ async def test_get_configtree_success(client, mocker: AsyncMock): # noqa: F811 ) # Call the get_configtree method - response = await client.get_configtree(name="mock_configtree_name") + response = await async_client.get_configtree(name="mock_configtree_name") # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_configtree_guid" - assert response.metadata.name == "test_configtree" + assert response["metadata"]["guid"] == "test_configtree_guid" + assert response["metadata"]["name"] == "test_configtree" @pytest.mark.asyncio -async def test_set_configtree_revision_success(client, mocker: AsyncMock): # noqa: F811 +async def test_set_configtree_revision_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.put method mock_put = mocker.patch("httpx.AsyncClient.put") @@ -125,42 +122,34 @@ async def test_set_configtree_revision_success(client, mocker: AsyncMock): # no ) # Call the set_configtree_revision method - response = await client.set_configtree_revision( + response = await async_client.set_configtree_revision( name="mock_configtree_name", configtree=configtree_body ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_configtree_guid" - assert response.metadata.name == "test_configtree" + assert response["metadata"]["guid"] == "test_configtree_guid" + assert response["metadata"]["name"] == "test_configtree" @pytest.mark.asyncio -async def test_update_configtree_success(client, mocker: AsyncMock): # noqa: F811 +async def test_update_configtree_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.put method mock_put = mocker.patch("httpx.AsyncClient.put") - - # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_configtree_guid", "name": "test_configtree"}, }, ) - - # Call the update_configtree method - response = await client.update_configtree( + response = await async_client.update_configtree( name="mock_configtree_name", body=configtree_body ) - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_configtree_guid" - assert response.metadata.name == "test_configtree" + assert response["metadata"]["guid"] == "test_configtree_guid" + assert response["metadata"]["name"] == "test_configtree" @pytest.mark.asyncio -async def test_delete_configtree_success(client, mocker: AsyncMock): # noqa: F811 +async def test_delete_configtree_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.delete method mock_delete = mocker.patch("httpx.AsyncClient.delete") @@ -171,14 +160,14 @@ async def test_delete_configtree_success(client, mocker: AsyncMock): # noqa: F8 ) # Call the delete_configtree method - response = await client.delete_configtree(name="mock_configtree_name") + response = await async_client.delete_configtree(name="mock_configtree_name") # Validate the response - assert response["success"] is True + assert response is None @pytest.mark.asyncio -async def test_list_revisions_success(client, mocker: AsyncMock): # noqa: F811 +async def test_list_revisions_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") @@ -192,17 +181,16 @@ async def test_list_revisions_success(client, mocker: AsyncMock): # noqa: F811 ) # Call the list_revisions method - response = await client.list_revisions(tree_name="mock_configtree_name") + response = await async_client.list_revisions(tree_name="mock_configtree_name") # Validate the response - assert isinstance(response, Munch) assert response["items"] == [ {"name": "test-configtree", "guid": "mock_configtree_guid"} ] @pytest.mark.asyncio -async def test_create_revision_success(client, mocker: AsyncMock): # noqa: F811 +async def test_create_revision_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.post method mock_post = mocker.patch("httpx.AsyncClient.post") @@ -215,17 +203,16 @@ async def test_create_revision_success(client, mocker: AsyncMock): # noqa: F811 ) # Call the create_revision method - response = await client.create_revision( + response = await async_client.create_revision( name="mock_configtree_name", body=configtree_body ) # Validate the response - assert isinstance(response, Munch) assert response["metadata"]["guid"] == "test_revision_guid" @pytest.mark.asyncio -async def test_put_keys_in_revision_success(client, mocker: AsyncMock): # noqa: F811 +async def test_put_keys_in_revision_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.put method mock_put = mocker.patch("httpx.AsyncClient.put") @@ -238,20 +225,19 @@ async def test_put_keys_in_revision_success(client, mocker: AsyncMock): # noqa: ) # Call the put_keys_in_revision method - response = await client.put_keys_in_revision( + response = await async_client.put_keys_in_revision( name="mock_configtree_name", revision_id="mock_revision_id", config_values=["mock_value1", "mock_value2"], ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" @pytest.mark.asyncio -async def test_commit_revision_success(client, mocker: AsyncMock): # noqa: F811 +async def test_commit_revision_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.put method mock_patch = mocker.patch("httpx.AsyncClient.patch") @@ -264,19 +250,18 @@ async def test_commit_revision_success(client, mocker: AsyncMock): # noqa: F811 ) # Call the commit_revision method - response = await client.commit_revision( + response = await async_client.commit_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" @pytest.mark.asyncio -async def test_get_key_in_revision(client, mocker: AsyncMock): # noqa: F811 +async def test_get_key_in_revision(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") @@ -289,18 +274,17 @@ async def test_get_key_in_revision(client, mocker: AsyncMock): # noqa: F811 ) # Call the get_key_in_revision method - response = await client.get_key_in_revision( + response = await async_client.get_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key" ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" @pytest.mark.asyncio -async def test_put_key_in_revision_success(client, mocker: AsyncMock): # noqa: F811 +async def test_put_key_in_revision_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.put method mock_put = mocker.patch("httpx.AsyncClient.put") @@ -313,18 +297,17 @@ async def test_put_key_in_revision_success(client, mocker: AsyncMock): # noqa: ) # Call the put_key_in_revision method - response = await client.put_key_in_revision( + response = await async_client.put_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key" ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" @pytest.mark.asyncio -async def test_delete_key_in_revision_success(client, mocker: AsyncMock): # noqa: F811 +async def test_delete_key_in_revision_success(async_client, mocker: AsyncMock): mock_delete = mocker.patch("httpx.AsyncClient.delete") mock_delete.return_value = httpx.Response( @@ -332,15 +315,15 @@ async def test_delete_key_in_revision_success(client, mocker: AsyncMock): # noq json={"success": True}, ) - response = await client.delete_key_in_revision( + response = await async_client.delete_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key" ) - assert response["success"] is True + assert response is None @pytest.mark.asyncio -async def test_rename_key_in_revision_success(client, mocker: AsyncMock): # noqa: F811 +async def test_rename_key_in_revision_success(async_client, mocker: AsyncMock): mock_patch = mocker.patch("httpx.AsyncClient.patch") mock_patch.return_value = httpx.Response( @@ -350,13 +333,13 @@ async def test_rename_key_in_revision_success(client, mocker: AsyncMock): # noq }, ) - response = await client.rename_key_in_revision( + response = await async_client.rename_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key", config_key_rename={"metadata": {"name": "test_key"}}, ) - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert isinstance(response, dict) + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" diff --git a/tests/async_tests/test_deployment_async.py b/tests/async_tests/test_deployment_async.py index f124148..82971e1 100644 --- a/tests/async_tests/test_deployment_async.py +++ b/tests/async_tests/test_deployment_async.py @@ -1,153 +1,138 @@ import httpx import pytest -import pytest_asyncio # noqa: F401 -from munch import Munch -from asyncmock import AsyncMock +from pytest_mock import MockFixture -from tests.data.mock_data import deployment_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import DeploymentList, Deployment +from tests.utils.fixtures import async_client +from tests.data import ( + deployment_body, + deploymentlist_model_mock, + cloud_deployment_model_mock, + device_deployment_model_mock, +) @pytest.mark.asyncio -async def test_list_deployments_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get") method +async def test_list_deployments_success( + async_client, deploymentlist_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-deployment", "guid": "mock_deployment_guid"}], - }, + json=deploymentlist_model_mock, ) - # Call the list_deployments method - response = await client.list_deployments() + response = await async_client.list_deployments() - # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [ - {"name": "test-deployment", "guid": "mock_deployment_guid"} - ] + assert isinstance(response, DeploymentList) + assert response.metadata.continue_ == 123 + assert len(response.items) == 2 + cloud_dep = response.items[0] + device_dep = response.items[1] + assert cloud_dep.spec.runtime == "cloud" + assert device_dep.spec.runtime == "device" + assert cloud_dep.metadata.guid == "dep-cloud-001" + assert device_dep.metadata.guid == "dep-device-001" @pytest.mark.asyncio -async def test_list_deployments_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get") method +async def test_list_deployments_not_found(async_client, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=404, json={"error": "not found"}, ) with pytest.raises(Exception) as exc: - await client.list_deployments() + await async_client.list_deployments() assert str(exc.value) == "not found" @pytest.mark.asyncio -async def test_get_deployment_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get") method +async def test_get_cloud_deployment_success( + async_client, cloud_deployment_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Deployment", - "metadata": {"guid": "test_deployment_guid", "name": "test_deployment"}, - }, + json=cloud_deployment_model_mock, ) + response = await async_client.get_deployment(name="cloud_deployment_sample") + assert isinstance(response, Deployment) + assert response.spec.runtime == "cloud" + assert response.metadata.guid == "dep-cloud-001" - # Call the get_deployment method - response = await client.get_deployment(name="mock_deployment_name") - # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_deployment_guid" +@pytest.mark.asyncio +async def test_get_device_deployment_success( + async_client, device_deployment_model_mock, mocker: MockFixture +): + mock_get = mocker.patch("httpx.AsyncClient.get") + mock_get.return_value = httpx.Response( + status_code=200, + json=device_deployment_model_mock, + ) + response = await async_client.get_deployment(name="device_deployment_sample") + assert isinstance(response, Deployment) + assert response.spec.runtime == "device" + assert response.metadata.guid == "dep-device-001" @pytest.mark.asyncio -async def test_get_deployment_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get") method +async def test_get_deployment_not_found(async_client, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=404, json={"error": "deployment not found"}, ) - # Call the get_deployment method with pytest.raises(Exception) as exc: - await client.get_deployment(name="mock_deployment_name") + await async_client.get_deployment(name="mock_deployment_name") assert str(exc.value) == "deployment not found" @pytest.mark.asyncio -async def test_create_deployment_success(client, deployment_body, mocker: AsyncMock): # noqa: F811 - mock_post = mocker.patch("httpx.AsyncClient.post") - - mock_post.return_value = httpx.Response( - status_code=200, - json={ - "kind": "Deployment", - "metadata": {"guid": "test_deployment_guid", "name": "test_deployment"}, - }, - ) - - response = await client.create_deployment(body=deployment_body) - - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_deployment_guid" - - -@pytest.mark.asyncio -async def test_create_deployment_unauthorized(client, deployment_body, mocker: AsyncMock): # noqa: F811 +async def test_create_deployment_unauthorized( + async_client, deployment_body, mocker: MockFixture +): mock_post = mocker.patch("httpx.AsyncClient.post") - mock_post.return_value = httpx.Response( status_code=401, json={"error": "unauthorized"}, ) with pytest.raises(Exception) as exc: - await client.create_deployment(body=deployment_body) + await async_client.create_deployment(body=deployment_body) assert str(exc.value) == "unauthorized" @pytest.mark.asyncio -async def test_update_deployment_success(client, deployment_body, mocker: AsyncMock): # noqa: F811 +async def test_update_deployment_success( + async_client, deployment_body, device_deployment_model_mock, mocker: MockFixture +): mock_put = mocker.patch("httpx.AsyncClient.put") - mock_put.return_value = httpx.Response( status_code=200, - json={ - "kind": "Deployment", - "metadata": {"guid": "test_deployment_guid", "name": "test_deployment"}, - }, + json=device_deployment_model_mock, ) - response = await client.update_deployment( - name="mock_deployment_name", body=deployment_body + response = await async_client.update_deployment( + name="device_deployment_sample", body=deployment_body ) - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_deployment_guid" + assert isinstance(response, Deployment) + assert response.metadata.guid == "dep-device-001" @pytest.mark.asyncio -async def test_delete_deployment_success(client, mocker: AsyncMock): # noqa: F811 +async def test_delete_deployment_success(async_client, mocker: MockFixture): mock_delete = mocker.patch("httpx.AsyncClient.delete") - mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) - response = await client.delete_deployment(name="mock_deployment_name") + response = await async_client.delete_deployment(name="mock_deployment_name") - assert response["success"] is True + assert response is None diff --git a/tests/async_tests/test_disk_async.py b/tests/async_tests/test_disk_async.py index 78f7d1a..ce66964 100644 --- a/tests/async_tests/test_disk_async.py +++ b/tests/async_tests/test_disk_async.py @@ -1,142 +1,77 @@ import httpx import pytest -import pytest_asyncio # noqa: F401 -from munch import Munch -from asyncmock import AsyncMock +from pytest_mock import MockFixture -from tests.data.mock_data import disk_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import DiskList, Disk +from tests.utils.fixtures import async_client +from tests.data import ( + disk_body, + disk_model_mock, + disklist_model_mock, +) @pytest.mark.asyncio -async def test_list_disks_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_list_disks_success(async_client, disklist_model_mock, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-disk", "guid": "mock_disk_guid"}], - }, + json=disklist_model_mock, ) - # Call the list_disks method - response = await client.list_disks() + response = await async_client.list_disks() - # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-disk", "guid": "mock_disk_guid"}] + assert isinstance(response, DiskList) + assert len(response.items) == 1 + assert response.items[0].metadata.name == "mock_disk_1" @pytest.mark.asyncio -async def test_list_disks_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_get_disk_success(async_client, disk_model_mock, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response - mock_get.return_value = httpx.Response( - status_code=404, - json={"error": "not found"}, - ) - - with pytest.raises(Exception) as exc: - await client.list_disks() - - assert str(exc.value) == "not found" - - -@pytest.mark.asyncio -async def test_get_disk_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method - mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Disk", - "metadata": {"guid": "test_disk_guid", "name": "mock_disk_name"}, - }, + json=disk_model_mock, ) - - # Call the get_disk method - response = await client.get_disk(name="mock_disk_name") - - # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_disk_guid" + response = await async_client.get_disk(name="mock_disk_1") + assert isinstance(response, Disk) + assert response.metadata.name == "mock_disk_1" @pytest.mark.asyncio -async def test_get_disk_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_get_disk_not_found(async_client, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=404, json={"error": "disk not found"}, ) - # Call the get_disk method with pytest.raises(Exception) as exc: - await client.get_disk(name="mock_disk_name") + await async_client.get_disk(name="notfound") assert str(exc.value) == "disk not found" @pytest.mark.asyncio -async def test_create_disk_success(client, disk_body, mocker: AsyncMock): # noqa: F811 +async def test_create_disk_unauthorized(async_client, disk_body, mocker: MockFixture): mock_post = mocker.patch("httpx.AsyncClient.post") - mock_post.return_value = httpx.Response( - status_code=200, - json={ - "kind": "Disk", - "metadata": {"guid": "test_disk_guid", "name": "test_disk"}, - }, - ) - - response = await client.create_disk(body=disk_body, project_guid="mock_project_guid") - - assert isinstance(response, Munch) - assert response.metadata.guid == "test_disk_guid" - assert response.metadata.name == "test_disk" - - -@pytest.mark.asyncio -async def test_delete_disk_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.delete method - mock_delete = mocker.patch("httpx.AsyncClient.delete") - - # Set up the mock response - mock_delete.return_value = httpx.Response( - status_code=204, - json={"success": True}, + status_code=401, + json={"error": "unauthorized"}, ) - # Call the delete_disk method - response = await client.delete_disk(name="mock_disk_name") + with pytest.raises(Exception) as exc: + await async_client.create_disk(body=disk_body) - # Validate the response - assert response["success"] is True + assert str(exc.value) == "unauthorized" @pytest.mark.asyncio -async def test_delete_disk_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.delete method +async def test_delete_disk_success(async_client, mocker: MockFixture): mock_delete = mocker.patch("httpx.AsyncClient.delete") + mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) - # Set up the mock response - mock_delete.return_value = httpx.Response( - status_code=404, - json={"error": "disk not found"}, - ) - - # Call the delete_disk method - with pytest.raises(Exception) as exc: - await client.delete_disk(name="mock_disk_name") + response = await async_client.delete_disk(name="mock_disk_1") - assert str(exc.value) == "disk not found" + assert response is None diff --git a/tests/async_tests/test_managedservice_async.py b/tests/async_tests/test_managedservice_async.py index d670583..e280dff 100644 --- a/tests/async_tests/test_managedservice_async.py +++ b/tests/async_tests/test_managedservice_async.py @@ -1,13 +1,27 @@ import httpx -import pytest # noqa: F401 -from munch import Munch +import pytest from asyncmock import AsyncMock -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import ( + ManagedServiceBinding, + ManagedServiceInstanceList, + ManagedServiceBindingList, + ManagedServiceInstance, + ManagedServiceProvider, + ManagedServiceProviderList, +) +from tests.utils.fixtures import async_client +from tests.data import ( + managedservice_binding_model_mock, + managedservice_model_mock, + managedservicebindinglist_model_mock, + managedservicelist_model_mock, +) @pytest.mark.asyncio -async def test_list_providers_success(client, mocker: AsyncMock): # noqa: F811 +async def test_list_providers_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") @@ -21,79 +35,98 @@ async def test_list_providers_success(client, mocker: AsyncMock): # noqa: F811 ) # Call the list_providers method - response = await client.list_providers() + response = await async_client.list_providers() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-provider", "guid": "mock_provider_guid"}] + assert isinstance(response, ManagedServiceProviderList) + assert isinstance(response.items[0], ManagedServiceProvider) + assert response.items[0].name == "test-provider" @pytest.mark.asyncio -async def test_list_instances_success(client, mocker: AsyncMock): # noqa: F811 +async def test_list_instances_success( + async_client, managedservicelist_model_mock, mocker: AsyncMock +): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-instance", "guid": "mock_instance_guid"}], - }, + json=managedservicelist_model_mock, ) # Call the list_instances method - response = await client.list_instances() + response = await async_client.list_instances() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-instance", "guid": "mock_instance_guid"}] + assert isinstance(response, ManagedServiceInstanceList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + instance = response.items[0] + assert instance.metadata.guid == "mock_instance_guid" + assert instance.metadata.name == "test-instance" + assert instance.kind == "ManagedServiceInstance" + assert instance.spec.provider == "elasticsearch" + assert instance.spec.config["version"] == "7.10" @pytest.mark.asyncio -async def test_get_instance_success(client, mocker: AsyncMock): # noqa: F811 +async def test_get_instance_success( + async_client, managedservice_model_mock, mocker: AsyncMock +): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_instance_guid", "name": "test_instance"}, - }, + json=managedservice_model_mock, ) # Call the get_instance method - response = await client.get_instance(name="mock_instance_name") + response = await async_client.get_instance(name="mock_instance_name") # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_guid" + assert isinstance(response, ManagedServiceInstance) + assert response.metadata.guid == "mock_instance_guid" + assert response.metadata.name == "test-instance" + assert response.kind == "ManagedServiceInstance" + assert response.spec.provider == "elasticsearch" @pytest.mark.asyncio -async def test_create_instance_success(client, mocker: AsyncMock): # noqa: F811 +async def test_create_instance_success( + async_client, managedservice_model_mock, mocker: AsyncMock +): # Mock the httpx.AsyncClient.post method mock_post = mocker.patch("httpx.AsyncClient.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": {"guid": "test_instance_guid", "name": "test_instance"}, - }, + json=managedservice_model_mock, ) # Call the create_instance method - response = await client.create_instance(body={"name": "test_instance"}) + response = await async_client.create_instance( + body={ + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "name": "test-instance", + }, + } + ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_guid" + assert isinstance(response, ManagedServiceInstance) + assert response.metadata.guid == "mock_instance_guid" + assert response.metadata.name == "test-instance" + assert response.kind == "ManagedServiceInstance" @pytest.mark.asyncio -async def test_delete_instance_success(client, mocker: AsyncMock): # noqa: F811 +async def test_delete_instance_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.delete method mock_delete = mocker.patch("httpx.AsyncClient.delete") @@ -104,92 +137,104 @@ async def test_delete_instance_success(client, mocker: AsyncMock): # noqa: F811 ) # Call the delete_instance method - response = await client.delete_instance(name="mock_instance_name") + response = await async_client.delete_instance(name="mock_instance_name") # Validate the response - assert response["success"] is True + assert response is None @pytest.mark.asyncio -async def test_list_instance_bindings_success(client, mocker: AsyncMock): # noqa: F811 +async def test_list_instance_bindings_success( + async_client, managedservicebindinglist_model_mock, mocker: AsyncMock +): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [ - {"name": "test-instance-binding", "guid": "mock_instance_binding_guid"} - ], - }, + json=managedservicebindinglist_model_mock, ) # Call the list_instance_bindings method - response = await client.list_instance_bindings("mock_instance_name") + response = await async_client.list_instance_bindings( + instance_name="mock_instance_name" + ) # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [ - {"name": "test-instance-binding", "guid": "mock_instance_binding_guid"} - ] + assert isinstance(response, ManagedServiceBindingList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + binding = response.items[0] + assert binding.metadata.guid == "mock_instance_binding_guid" + assert binding.metadata.name == "test-instance-binding" + assert binding.kind == "ManagedServiceBinding" + assert binding.spec.provider == "headscalevpn" @pytest.mark.asyncio -async def test_get_instance_binding_success(client, mocker: AsyncMock): # noqa: F811 +async def test_get_instance_binding_success( + async_client, managedservice_binding_model_mock, mocker: AsyncMock +): # Mock the httpx.AsyncClient.get method mock_get = mocker.patch("httpx.AsyncClient.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": { - "guid": "test_instance_binding_guid", - "name": "test_instance_binding", - }, - }, + json=managedservice_binding_model_mock, ) # Call the get_instance_binding method - response = await client.get_instance_binding( - name="mock_instance_binding_name", instance_name="mock_instance_name" + response = await async_client.get_instance_binding( + name="test-instance-binding", instance_name="mock_instance_name" ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_binding_guid" + assert isinstance(response, ManagedServiceBinding) + assert response.metadata.guid == "mock_instance_binding_guid" + assert response.metadata.name == "test-instance-binding" + assert response.kind == "ManagedServiceBinding" + assert response.spec.provider == "headscalevpn" @pytest.mark.asyncio -async def test_create_instance_binding_success(client, mocker: AsyncMock): # noqa: F811 +async def test_create_instance_binding_success( + async_client, managedservice_binding_model_mock, mocker: AsyncMock +): # Mock the httpx.AsyncClient.post method mock_post = mocker.patch("httpx.AsyncClient.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": { - "guid": "test_instance_binding_guid", - "name": "test_instance_binding", - }, - }, + json=managedservice_binding_model_mock, ) # Call the create_instance_binding method - response = await client.create_instance_binding( - body={"name": "test_instance_binding"}, instance_name="mock_instance_name" + response = await async_client.create_instance_binding( + body={ + "metadata": { + "name": "test-instance-binding", + "labels": {}, + }, + "spec": { + "instance": "vpn_instance_value", + "provider": "headscalevpn", + }, + }, + instance_name="mock_instance_name", ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_binding_guid" + assert isinstance(response, ManagedServiceBinding) + assert response.metadata.guid == "mock_instance_binding_guid" + assert response.metadata.name == "test-instance-binding" + assert response.kind == "ManagedServiceBinding" @pytest.mark.asyncio -async def test_delete_instance_binding_success(client, mocker: AsyncMock): # noqa: F811 +async def test_delete_instance_binding_success(async_client, mocker: AsyncMock): # Mock the httpx.AsyncClient.delete method mock_delete = mocker.patch("httpx.AsyncClient.delete") @@ -200,9 +245,9 @@ async def test_delete_instance_binding_success(client, mocker: AsyncMock): # no ) # Call the delete_instance_binding method - response = await client.delete_instance_binding( + response = await async_client.delete_instance_binding( name="mock_instance_binding_name", instance_name="mock_instance_name" ) # Validate the response - assert response["success"] is True + assert response is None diff --git a/tests/async_tests/test_network_async.py b/tests/async_tests/test_network_async.py index 0512404..bba5294 100644 --- a/tests/async_tests/test_network_async.py +++ b/tests/async_tests/test_network_async.py @@ -1,124 +1,81 @@ import httpx import pytest -import pytest_asyncio # noqa: F401 -from munch import Munch -from asyncmock import AsyncMock +from pytest_mock import MockFixture -from tests.data.mock_data import network_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import NetworkList, Network +from tests.utils.fixtures import async_client +from tests.data import ( + network_body, + network_model_mock, + networklist_model_mock, +) @pytest.mark.asyncio -async def test_list_networks_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_list_networks_success( + async_client, networklist_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-network", "guid": "mock_network_guid"}], - }, + json=networklist_model_mock, ) - # Call the list_networks method - response = await client.list_networks() + response = await async_client.list_networks() - # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-network", "guid": "mock_network_guid"}] + assert isinstance(response, NetworkList) + assert len(response.items) == 1 + assert response.items[0].metadata.name == "test-network" @pytest.mark.asyncio -async def test_list_networks_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_get_network_success(async_client, network_model_mock, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( - status_code=404, - json={"error": "not found"}, + status_code=200, + json=network_model_mock, ) - - with pytest.raises(Exception) as exc: - await client.list_networks() - - assert str(exc.value) == "not found" + response = await async_client.get_network(name="test-network") + assert isinstance(response, Network) + assert response.metadata.name == "test-network" @pytest.mark.asyncio -async def test_create_network_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.post method - mock_post = mocker.patch("httpx.AsyncClient.post") - - # Set up the mock response - mock_post.return_value = httpx.Response( - status_code=201, - json={ - "metadata": {"guid": "mock_network_guid", "name": "test-network"}, - }, +async def test_get_network_not_found(async_client, mocker: MockFixture): + mock_get = mocker.patch("httpx.AsyncClient.get") + mock_get.return_value = httpx.Response( + status_code=404, + json={"error": "network not found"}, ) - # Call the create_network method - response = await client.create_network(body=network_body) + with pytest.raises(Exception) as exc: + await async_client.get_network(name="notfound") - # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["name"] == "test-network" + assert str(exc.value) == "network not found" @pytest.mark.asyncio -async def test_create_network_failure(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.post method +async def test_create_network_unauthorized( + async_client, network_body, mocker: MockFixture +): mock_post = mocker.patch("httpx.AsyncClient.post") - - # Set up the mock response mock_post.return_value = httpx.Response( - status_code=409, - json={"error": "already exists"}, + status_code=401, + json={"error": "unauthorized"}, ) with pytest.raises(Exception) as exc: - await client.create_network(body=network_body) + await async_client.create_network(body=network_body) - assert str(exc.value) == "already exists" + assert str(exc.value) == "unauthorized" @pytest.mark.asyncio -async def test_get_network_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method - mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response - mock_get.return_value = httpx.Response( - status_code=200, - json={ - "metadata": {"guid": "mock_network_guid", "name": "test-network"}, - }, - ) - - # Call the get_network method - response = await client.get_network(name="test-network") - - # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_network_guid" - - -@pytest.mark.asyncio -async def test_delete_network_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.delete method +async def test_delete_network_success(async_client, mocker: MockFixture): mock_delete = mocker.patch("httpx.AsyncClient.delete") + mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) - # Set up the mock response - mock_delete.return_value = httpx.Response( - status_code=204, - json={"success": True}, - ) - - # Call the delete_network method - response = await client.delete_network(name="test-network") + response = await async_client.delete_network(name="test-network") - # Validate the response - assert response["success"] is True + assert response is None diff --git a/tests/async_tests/test_organization_async.py b/tests/async_tests/test_organization_async.py index 2505554..a55ec52 100644 --- a/tests/async_tests/test_organization_async.py +++ b/tests/async_tests/test_organization_async.py @@ -1,31 +1,40 @@ from asyncmock import AsyncMock import httpx import pytest -from munch import Munch -from tests.data.mock_data import mock_response_organization, organization_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 + +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models.organization import Organization +from tests.data.mock_data import mock_response_organization, organization_body +from tests.utils.fixtures import async_client as client @pytest.mark.asyncio -async def test_get_organization_success(client, mocker: AsyncMock): # noqa: F811 +async def test_get_organization_success( + client, mock_response_organization, mocker: AsyncMock +): mock_get = mocker.patch("httpx.AsyncClient.get") + # Use mock_response_organization fixture for GET response mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Organization", - "metadata": {"name": "test-org", "guid": "mock_org_guid"}, - }, + json=mock_response_organization, ) response = await client.get_organization() - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test-org", "guid": "mock_org_guid"} + # Validate that response is an Organization model object + assert isinstance(response, Organization) + assert response.metadata.name == "test-org" + assert response.metadata.guid == "mock_org_guid" + assert len(response.spec.users) == 2 + assert response.spec.users[0].emailID == "test.user1@rapyuta-robotics.com" + assert response.spec.users[0].roleInOrganization == "viewer" + assert response.spec.users[1].emailID == "test.user2@rapyuta-robotics.com" + assert response.spec.users[1].roleInOrganization == "admin" @pytest.mark.asyncio -async def test_get_organization_unauthorized(client, mocker: AsyncMock): # noqa: F811 +async def test_get_organization_unauthorized(client, mocker: AsyncMock): mock_get = mocker.patch("httpx.AsyncClient.get") mock_get.return_value = httpx.Response( @@ -41,8 +50,9 @@ async def test_get_organization_unauthorized(client, mocker: AsyncMock): # noqa @pytest.mark.asyncio async def test_update_organization_success( - client, # noqa: F811 - mock_response_organization, # noqa: F811 + client, + mock_response_organization, + organization_body, mocker: AsyncMock, ): mock_put = mocker.patch("httpx.AsyncClient.put") @@ -57,5 +67,10 @@ async def test_update_organization_success( body=organization_body, ) - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test-org", "guid": "mock_org_guid"} + # Validate that response is an Organization model object + assert isinstance(response, Organization) + assert response.metadata.name == "test-org" + assert response.metadata.guid == "mock_org_guid" + assert len(response.spec.users) == 2 + assert response.spec.users[0].roleInOrganization == "viewer" + assert response.spec.users[1].roleInOrganization == "admin" diff --git a/tests/async_tests/test_package_async.py b/tests/async_tests/test_package_async.py index cc40cea..d7ce9ac 100644 --- a/tests/async_tests/test_package_async.py +++ b/tests/async_tests/test_package_async.py @@ -1,106 +1,99 @@ -import pytest -import pytest_asyncio # noqa: F401 import httpx -from munch import Munch -from asyncmock import AsyncMock +import pytest +from pytest_mock import MockFixture -from tests.data.mock_data import package_body -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import PackageList, Package +from tests.utils.fixtures import async_client +from tests.data import ( + package_body, + cloud_package_model_mock, + device_package_model_mock, + packagelist_model_mock, +) @pytest.mark.asyncio -async def test_list_packages_success(client, mocker: AsyncMock): # noqa: F811 +async def test_list_packages_success( + async_client, packagelist_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test_package", "guid": "mock_package_guid"}], - }, + json=packagelist_model_mock, ) - response = await client.list_packages() + response = await async_client.list_packages() - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test_package", "guid": "mock_package_guid"}] + assert isinstance(response, PackageList) + assert len(response.items) == 2 + assert response.items[0].metadata.name == "gostproxy" + assert response.items[1].metadata.name == "database" @pytest.mark.asyncio -async def test_list_packages_not_found(client, mocker: AsyncMock): # noqa: F811 +async def test_get_cloud_package_success( + async_client, cloud_package_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - mock_get.return_value = httpx.Response( - status_code=404, - json={"error": "not found"}, + status_code=200, + json=cloud_package_model_mock, ) - - with pytest.raises(Exception) as exc: - await client.list_packages() - assert str(exc.value) == "not found" + response = await async_client.get_package(name="gostproxy") + assert isinstance(response, Package) + assert response.metadata.name == "gostproxy" @pytest.mark.asyncio -async def test_create_package_success(client, mocker: AsyncMock): # noqa: F811 - mock_post = mocker.patch("httpx.AsyncClient.post") - - mock_post.return_value = httpx.Response( +async def test_get_device_package_success( + async_client, device_package_model_mock, mocker: MockFixture +): + mock_get = mocker.patch("httpx.AsyncClient.get") + mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Package", - "metadata": {"name": "test-package", "guid": "mock_package_guid"}, - "spec": {"users": [{"userGUID": "mock_user_guid", "emailID": "mock_email"}]}, - }, + json=device_package_model_mock, ) - - response = await client.create_package(package_body) - - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_package_guid" + response = await async_client.get_package(name="database") + assert isinstance(response, Package) + assert response.metadata.name == "database" @pytest.mark.asyncio -async def test_get_package_success(client, mocker: AsyncMock): # noqa: F811 +async def test_get_package_not_found(async_client, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - mock_get.return_value = httpx.Response( - status_code=200, - json={ - "kind": "Package", - "metadata": {"name": "test-package", "guid": "mock_package_guid"}, - "spec": {"users": [{"userGUID": "mock_user_guid", "emailID": "mock_email"}]}, - }, + status_code=404, + json={"error": "package not found"}, ) - response = await client.get_package("mock_package_guid") + with pytest.raises(Exception) as exc: + await async_client.get_package(name="notfound") - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_package_guid" + assert str(exc.value) == "package not found" @pytest.mark.asyncio -async def test_get_package_not_found(client, mocker: AsyncMock): # noqa: F811 - mock_get = mocker.patch("httpx.AsyncClient.get") - - mock_get.return_value = httpx.Response( - status_code=404, - json={"error": "not found"}, +async def test_create_package_unauthorized( + async_client, package_body, mocker: MockFixture +): + mock_post = mocker.patch("httpx.AsyncClient.post") + mock_post.return_value = httpx.Response( + status_code=401, + json={"error": "unauthorized"}, ) with pytest.raises(Exception) as exc: - await client.get_package("mock_package_guid") - assert str(exc.value) == "not found" + await async_client.create_package(body=package_body) + + assert str(exc.value) == "unauthorized" @pytest.mark.asyncio -async def test_delete_package_success(client, mocker: AsyncMock): # noqa: F811 +async def test_delete_package_success(async_client, mocker: MockFixture): mock_delete = mocker.patch("httpx.AsyncClient.delete") + mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) - mock_delete.return_value = httpx.Response( - status_code=204, - json={"success": True}, - ) - - response = await client.delete_package("mock_package_guid") + response = await async_client.delete_package(name="gostproxy", version="v1.0.0") - assert response["success"] is True + assert response is None diff --git a/tests/async_tests/test_project_async.py b/tests/async_tests/test_project_async.py index 8f4956c..73994e3 100644 --- a/tests/async_tests/test_project_async.py +++ b/tests/async_tests/test_project_async.py @@ -1,97 +1,99 @@ -import pytest -import pytest_asyncio # noqa: F401 import httpx -from munch import Munch -from asyncmock import AsyncMock +import pytest +from pytest_mock import MockFixture -from tests.data.mock_data import project_body -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import ProjectList, Project +from tests.utils.fixtures import async_client +from tests.data import ( + project_body, + project_model_mock, + projectlist_model_mock, +) @pytest.mark.asyncio -async def test_list_projects_success(client, mocker: AsyncMock): # noqa: F811 +async def test_list_projects_success( + async_client, projectlist_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-project", "guid": "mock_project_guid"}], - }, + json=projectlist_model_mock, ) - response = await client.list_projects() + response = await async_client.list_projects() - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-project", "guid": "mock_project_guid"}] + assert isinstance(response, ProjectList) + assert len(response.items) == 1 + assert response.items[0].metadata.name == "test-project" @pytest.mark.asyncio -async def test_create_project_success(client, mocker: AsyncMock): # noqa: F811 - mock_post = mocker.patch("httpx.AsyncClient.post") - - mock_post.return_value = httpx.Response( +async def test_get_project_success(async_client, project_model_mock, mocker: MockFixture): + mock_get = mocker.patch("httpx.AsyncClient.get") + mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Project", - "metadata": {"name": "test-project", "guid": "mock_project_guid"}, - "spec": { - "users": [ - {"userGUID": "mock_user_guid", "emailID": "test.user@example.com"} - ] - }, - }, + json=project_model_mock, ) - - response = await client.create_project(project_body) - - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_project_guid" + response = await async_client.get_project(project_guid="test-project") + assert isinstance(response, Project) + assert response.metadata.name == "test-project" @pytest.mark.asyncio -async def test_get_project_success(client, mocker: AsyncMock): # noqa: F811 +async def test_get_project_not_found(async_client, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - mock_get.return_value = httpx.Response( - status_code=200, - json={ - "kind": "Project", - "metadata": {"name": "test-project", "guid": "mock_project_guid"}, - }, + status_code=404, + json={"error": "project not found"}, ) - response = await client.get_project("mock_project_guid") + with pytest.raises(Exception) as exc: + await async_client.get_project(project_guid="notfound") - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_project_guid" + assert str(exc.value) == "project not found" @pytest.mark.asyncio -async def test_update_project_success(client, mocker: AsyncMock): # noqa: F811 - mock_put = mocker.patch("httpx.AsyncClient.put") +async def test_create_project_unauthorized( + async_client, project_body, mocker: MockFixture +): + mock_post = mocker.patch("httpx.AsyncClient.post") + mock_post.return_value = httpx.Response( + status_code=401, + json={"error": "unauthorized"}, + ) + + with pytest.raises(Exception) as exc: + await async_client.create_project(body=project_body) + + assert str(exc.value) == "unauthorized" + +@pytest.mark.asyncio +async def test_update_project_success( + async_client, project_body, project_model_mock, mocker: MockFixture +): + mock_put = mocker.patch("httpx.AsyncClient.put") mock_put.return_value = httpx.Response( status_code=200, - json={ - "kind": "Project", - "metadata": {"name": "test-project", "guid": "mock_project_guid"}, - }, + json=project_model_mock, ) - response = await client.update_project("mock_project_guid", project_body) + response = await async_client.update_project( + project_guid="test-project", body=project_body + ) - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_project_guid" + assert isinstance(response, Project) + assert response.metadata.name == "test-project" @pytest.mark.asyncio -async def test_delete_project_success(client, mocker: AsyncMock): # noqa: F811 +async def test_delete_project_success(async_client, mocker: MockFixture): mock_delete = mocker.patch("httpx.AsyncClient.delete") + mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) - mock_delete.return_value = httpx.Response(status_code=200, json={"success": True}) - - response = await client.delete_project("mock_project_guid") + response = await async_client.delete_project(project_guid="test-project") - assert isinstance(response, Munch) - assert response["success"] is True + assert response is None diff --git a/tests/async_tests/test_secret_async.py b/tests/async_tests/test_secret_async.py index 3e86a65..cc61745 100644 --- a/tests/async_tests/test_secret_async.py +++ b/tests/async_tests/test_secret_async.py @@ -1,146 +1,95 @@ import httpx import pytest -import pytest_asyncio # noqa: F401 -from munch import Munch -from asyncmock import AsyncMock +from pytest_mock import MockFixture -from tests.data.mock_data import secret_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import SecretList, Secret +from tests.utils.fixtures import async_client +from tests.data import ( + secret_body, + secret_model_mock, + secretlist_model_mock, +) @pytest.mark.asyncio -async def test_list_secrets_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_list_secrets_success( + async_client, secretlist_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-secret", "guid": "mock_secret_guid"}], - }, + json=secretlist_model_mock, ) - # Call the list_secrets method - response = await client.list_secrets() + response = await async_client.list_secrets() - # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-secret", "guid": "mock_secret_guid"}] + assert isinstance(response, SecretList) + assert len(response.items) == 1 + assert response.items[0].metadata.name == "test_secret" @pytest.mark.asyncio -async def test_list_secrets_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_get_secret_success(async_client, secret_model_mock, mocker: MockFixture): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( - status_code=404, - json={"error": "not found"}, + status_code=200, + json=secret_model_mock, ) - - with pytest.raises(Exception) as exc: - await client.list_secrets() - - assert str(exc.value) == "not found" + response = await async_client.get_secret(name="test_secret") + assert isinstance(response, Secret) + assert response.metadata.name == "test_secret" @pytest.mark.asyncio -async def test_create_secret_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.post method - mock_post = mocker.patch("httpx.AsyncClient.post") - - # Set up the mock response - mock_post.return_value = httpx.Response( - status_code=201, - json={ - "metadata": {"guid": "test_secret_guid", "name": "test_secret"}, - }, +async def test_get_secret_not_found(async_client, mocker: MockFixture): + mock_get = mocker.patch("httpx.AsyncClient.get") + mock_get.return_value = httpx.Response( + status_code=404, + json={"error": "secret not found"}, ) - # Call the create_secret method - response = await client.create_secret(secret_body) + with pytest.raises(Exception) as exc: + await async_client.get_secret(name="notfound") - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_secret_guid" + assert str(exc.value) == "secret not found" @pytest.mark.asyncio -async def test_create_secret_already_exists(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.post method +async def test_create_secret_unauthorized(async_client, secret_body, mocker: MockFixture): mock_post = mocker.patch("httpx.AsyncClient.post") - - # Set up the mock response mock_post.return_value = httpx.Response( - status_code=409, - json={"error": "secret already exists"}, + status_code=401, + json={"error": "unauthorized"}, ) with pytest.raises(Exception) as exc: - await client.create_secret(secret_body) + await async_client.create_secret(body=secret_body) - assert str(exc.value) == "secret already exists" + assert str(exc.value) == "unauthorized" @pytest.mark.asyncio -async def test_update_secret_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.put method +async def test_update_secret_success( + async_client, secret_body, secret_model_mock, mocker: MockFixture +): mock_put = mocker.patch("httpx.AsyncClient.put") - - # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_secret_guid", "name": "test_secret"}, - }, + json=secret_model_mock, ) - # Call the update_secret method - response = await client.update_secret("mock_secret_guid", body=secret_body) + response = await async_client.update_secret(name="test_secret", body=secret_body) - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_secret_guid" + assert isinstance(response, Secret) + assert response.metadata.name == "test_secret" @pytest.mark.asyncio -async def test_delete_secret_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.delete method +async def test_delete_secret_success(async_client, mocker: MockFixture): mock_delete = mocker.patch("httpx.AsyncClient.delete") + mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) - # Set up the mock response - mock_delete.return_value = httpx.Response( - status_code=204, - json={"success": True}, - ) - - # Call the delete_secret method - response = await client.delete_secret("mock_secret_guid") + response = await async_client.delete_secret(name="test_secret") - # Validate the response - assert response == {"success": True} - - -@pytest.mark.asyncio -async def test_get_secret_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method - mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response - mock_get.return_value = httpx.Response( - status_code=200, - json={ - "metadata": {"guid": "test_secret_guid", "name": "test_secret"}, - }, - ) - - # Call the get_secret method - response = await client.get_secret("mock_secret_guid") - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_secret_guid" - assert response.metadata.name == "test_secret" + assert response is None diff --git a/tests/async_tests/test_staticroute_async.py b/tests/async_tests/test_staticroute_async.py index 91e7da3..6f52a83 100644 --- a/tests/async_tests/test_staticroute_async.py +++ b/tests/async_tests/test_staticroute_async.py @@ -1,148 +1,101 @@ import httpx import pytest -from munch import Munch -from asyncmock import AsyncMock +from pytest_mock import MockFixture -from tests.data.mock_data import staticroute_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import StaticRouteList, StaticRoute +from tests.utils.fixtures import async_client +from tests.data import ( + staticroute_body, + staticroute_model_mock, + staticroutelist_model_mock, +) @pytest.mark.asyncio -async def test_list_staticroutes_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_list_staticroutes_success( + async_client, staticroutelist_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-staticroute", "guid": "mock_staticroute_guid"}], - }, + json=staticroutelist_model_mock, ) - # Call the list_staticroutes method - response = await client.list_staticroutes() + response = await async_client.list_staticroutes() - # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [ - {"name": "test-staticroute", "guid": "mock_staticroute_guid"} - ] + assert isinstance(response, StaticRouteList) + assert len(response.items) == 1 + assert response.items[0].metadata.name == "test-staticroute" @pytest.mark.asyncio -async def test_list_staticroutes_not_found(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method +async def test_get_staticroute_success( + async_client, staticroute_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response mock_get.return_value = httpx.Response( - status_code=404, - json={"error": "not found"}, + status_code=200, + json=staticroute_model_mock, ) - - with pytest.raises(Exception) as exc: - await client.list_staticroutes() - - assert str(exc.value) == "not found" + response = await async_client.get_staticroute(name="test-staticroute") + assert isinstance(response, StaticRoute) + assert response.metadata.name == "test-staticroute" @pytest.mark.asyncio -async def test_create_staticroute_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.post method - mock_post = mocker.patch("httpx.AsyncClient.post") - - # Set up the mock response - mock_post.return_value = httpx.Response( - status_code=201, - json={ - "metadata": {"guid": "test_staticroute_guid", "name": "test_staticroute"}, - }, +async def test_get_staticroute_not_found(async_client, mocker: MockFixture): + mock_get = mocker.patch("httpx.AsyncClient.get") + mock_get.return_value = httpx.Response( + status_code=404, + json={"error": "staticroute not found"}, ) - # Call the create_staticroute method - response = await client.create_staticroute(body=staticroute_body) + with pytest.raises(Exception) as exc: + await async_client.get_staticroute(name="notfound") - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_staticroute_guid" + assert str(exc.value) == "staticroute not found" @pytest.mark.asyncio -async def test_create_staticroute_bad_request(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.post method +async def test_create_staticroute_unauthorized( + async_client, staticroute_body, mocker: MockFixture +): mock_post = mocker.patch("httpx.AsyncClient.post") - - # Set up the mock response mock_post.return_value = httpx.Response( - status_code=409, - json={"error": "already exists"}, + status_code=401, + json={"error": "unauthorized"}, ) with pytest.raises(Exception) as exc: - await client.create_staticroute(body=staticroute_body) - - assert str(exc.value) == "already exists" - - -@pytest.mark.asyncio -async def test_get_staticroute_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.get method - mock_get = mocker.patch("httpx.AsyncClient.get") - - # Set up the mock response - mock_get.return_value = httpx.Response( - status_code=200, - json={ - "metadata": {"guid": "test_staticroute_guid", "name": "test_staticroute"}, - }, - ) - - # Call the get_staticroute method - response = await client.get_staticroute(name="mock_staticroute_name") + await async_client.create_staticroute(body=staticroute_body) - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_staticroute_guid" + assert str(exc.value) == "unauthorized" @pytest.mark.asyncio -async def test_update_staticroute_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.put method +async def test_update_staticroute_success( + async_client, staticroute_body, staticroute_model_mock, mocker: MockFixture +): mock_put = mocker.patch("httpx.AsyncClient.put") - - # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_staticroute_guid", "name": "test_staticroute"}, - }, + json=staticroute_model_mock, ) - # Call the update_staticroute method - response = await client.update_staticroute( - name="mock_staticroute_name", body=staticroute_body + response = await async_client.update_staticroute( + name="test-staticroute", body=staticroute_body ) - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_staticroute_guid" + assert isinstance(response, StaticRoute) + assert response.metadata.name == "test-staticroute" @pytest.mark.asyncio -async def test_delete_staticroute_success(client, mocker: AsyncMock): # noqa: F811 - # Mock the httpx.AsyncClient.delete method +async def test_delete_staticroute_success(async_client, mocker: MockFixture): mock_delete = mocker.patch("httpx.AsyncClient.delete") + mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) - # Set up the mock response - mock_delete.return_value = httpx.Response( - status_code=204, - json={"success": True}, - ) - - # Call the delete_staticroute method - response = await client.delete_staticroute(name="mock_staticroute_name") + response = await async_client.delete_staticroute(name="test-staticroute") - # Validate the response - assert response["success"] is True + assert response is None diff --git a/tests/async_tests/test_user_async.py b/tests/async_tests/test_user_async.py index d5c7630..2c9f359 100644 --- a/tests/async_tests/test_user_async.py +++ b/tests/async_tests/test_user_async.py @@ -1,32 +1,29 @@ -from asyncmock import AsyncMock import httpx import pytest -from munch import Munch -from tests.data.mock_data import mock_response_user, user_body # noqa: F401 -from tests.utils.fixtures import async_client as client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.exceptions import UnauthorizedAccessError +from tests.data.mock_data import mock_response_user as mock_response_user, user_body +from tests.utils.fixtures import async_client @pytest.mark.asyncio -async def test_get_user_success(client, mocker: AsyncMock): # noqa: F811 +async def test_get_user_success(async_client, mock_response_user, mocker): mock_get = mocker.patch("httpx.AsyncClient.get") mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "User", - "metadata": {"name": "test-org", "guid": "mock_org_guid"}, - }, + json=mock_response_user, ) - response = await client.get_user() - - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test-org", "guid": "mock_org_guid"} + response = await async_client.get_user() + assert response.metadata.name == "test user" + assert response.metadata.guid == "mock_user_guid" + assert response.spec.emailID == "test.user@example.com" @pytest.mark.asyncio -async def test_get_user_unauthorized(client, mocker: AsyncMock): # noqa: F811 +async def test_get_user_unauthorized(async_client, mocker): mock_get = mocker.patch("httpx.AsyncClient.get") mock_get.return_value = httpx.Response( @@ -34,35 +31,31 @@ async def test_get_user_unauthorized(client, mocker: AsyncMock): # noqa: F811 json={"error": "user cannot be authenticated"}, ) - with pytest.raises(Exception) as exc: - await client.get_user() - - assert str(exc.value) == "user cannot be authenticated" + with pytest.raises(UnauthorizedAccessError) as exc: + await async_client.get_user() + assert "user cannot be authenticated" in str(exc.value) @pytest.mark.asyncio -async def test_update_user_success( - client, # noqa: F811 - mock_response_user, # noqa: F811 - mocker: AsyncMock, -): +async def test_update_user_success(async_client, mock_response_user, user_body, mocker): mock_put = mocker.patch("httpx.AsyncClient.put") - mock_put.return_value = httpx.Response( status_code=200, json=mock_response_user, ) - - response = await client.update_user( - body=user_body, - ) - - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test user", "guid": "mock_user_guid"} + response = await async_client.update_user(body=user_body) + assert response.metadata.name == "test user" + assert response.metadata.guid == "mock_user_guid" + assert response.spec.emailID == "test.user@example.com" + assert response.metadata.name == "test user" + assert response.metadata.guid == "mock_user_guid" + assert response.spec.emailID == "test.user@example.com" @pytest.mark.asyncio -async def test_update_user_unauthorized(client, mocker: AsyncMock): # noqa: F811 +async def test_update_user_unauthorized( + async_client, mock_response_user, user_body, mocker +): mock_put = mocker.patch("httpx.AsyncClient.put") mock_put.return_value = httpx.Response( @@ -70,7 +63,6 @@ async def test_update_user_unauthorized(client, mocker: AsyncMock): # noqa: F81 json={"error": "user cannot be authenticated"}, ) - with pytest.raises(Exception) as exc: - await client.update_user(user_body) - - assert str(exc.value) == "user cannot be authenticated" + with pytest.raises(UnauthorizedAccessError) as exc: + await async_client.update_user(user_body) + assert "user cannot be authenticated" in str(exc.value) diff --git a/tests/data/__init__.py b/tests/data/__init__.py new file mode 100644 index 0000000..bbc59ca --- /dev/null +++ b/tests/data/__init__.py @@ -0,0 +1 @@ +from .mock_data import * # noqa: F403 diff --git a/tests/data/mock_data.py b/tests/data/mock_data.py index 4e83b51..bd644ab 100644 --- a/tests/data/mock_data.py +++ b/tests/data/mock_data.py @@ -1,154 +1,352 @@ +# Deployment and DeploymentList mocks using pydantic models import pytest -from rapyuta_io_sdk_v2 import Configuration + +from rapyuta_io_sdk_v2.config import Configuration + +# -------------------- PROJECT -------------------- @pytest.fixture -def mock_response_user(): +def mock_response_project(): return { - "kind": "User", - "metadata": {"name": "test user", "guid": "mock_user_guid"}, + "kind": "Project", + "metadata": {"name": "test-project", "guid": "mock_project_guid"}, "spec": { - "firstName": "test", - "lastName": "user", - "emailID": "test.user@rapyuta-robotics.com", - "projects": [ - { - "guid": "mock_project_guid", - "creator": "mock_user_guid", - "name": "test-project", - "organizationGUID": "mock_org_guid", - "organizationCreatorGUID": "mock_user_guid", - }, - ], - "organizations": [ - { - "guid": "mock_org_guid", - "name": "test-org", - "creator": "mock_user_guid", - "shortGUID": "abcde", - }, - ], + "users": [{"userGUID": "mock_user_guid", "emailID": "test.user@example.com"}] }, } @pytest.fixture -def user_body(): +def project_body(): return { - "kind": "User", - "metadata": {"name": "test user", "guid": "mock_user_guid"}, + "apiVersion": "api.rapyuta.io/v2", + "kind": "Project", + "metadata": { + "name": "test-project", + "labels": {"purpose": "testing", "version": "1.0"}, + }, "spec": { - "firstName": "test", - "lastName": "user", - "emailID": "test.user@rapyuta-robotics.com", + "users": [{"emailID": "tst.user@example.com", "role": "admin"}], + "features": {"vpn": {"enabled": False}}, }, } @pytest.fixture -def mock_response_organization(): +def project_model_mock(): return { - "kind": "Organization", - "metadata": {"name": "test-org", "guid": "mock_org_guid"}, + "apiVersion": "api.rapyuta.io/v2", + "kind": "Project", + "metadata": { + "guid": "mock_project_guid", + "name": "test-project", + "labels": {"purpose": "testing", "version": "1.0"}, + }, "spec": { - "users": [ - { - "guid": "mock_user1_guid", - "emailID": "test.user1@rapyuta-robotics.com", - "roleInOrganization": "viewer", - }, - { - "guid": "mock_user2_guid", - "emailID": "test.user2@rapyuta-robotics.com", - "roleInOrganization": "admin", - }, - ] + "users": [{"emailID": "test.user@example.com", "role": "admin"}], + "features": {"vpn": {"enabled": False}}, }, } @pytest.fixture -def organization_body(): +def projectlist_model_mock(project_model_mock): return { - "kind": "Organization", - "metadata": {"name": "test-org", "guid": "mock_org_guid"}, - "spec": { - "users": [ - { - "guid": "mock_user1_guid", - "emailID": "test.user1@rapyuta-robotics.com", - "roleInOrganization": "viewer", - }, - { - "guid": "mock_user2_guid", - "emailID": "test.user2@rapyuta-robotics.com", - "roleInOrganization": "admin", - }, - ] + "metadata": { + "continue": 1, }, + "items": [project_model_mock], } +# -------------------- PACKAGE -------------------- + + @pytest.fixture -def mock_response_project(): +def package_body(): return { - "kind": "Project", - "metadata": {"name": "test-project", "guid": "mock_project_guid"}, - "spec": { - "users": [{"userGUID": "mock_user_guid", "emailID": "test.user@example.com"}] + "apiVersion": "apiextensions.rapyuta.io/v1", + "kind": "Package", + "metadata": { + "name": "test-package", + "version": "v1.0.0", + "description": "Test package for demo", + "labels": {"app": "test"}, + "projectguid": "mock_project_guid", }, + "spec": {"runtime": "cloud", "cloud": {"enabled": True}}, } @pytest.fixture -def project_body(): +def cloud_package_model_mock(): return { - "apiVersion": "api.rapyuta.io/v2", - "kind": "Project", + "apiVersion": "apiextensions.rapyuta.io/v1", + "kind": "Package", "metadata": { - "name": "test-project", - "labels": {"purpose": "testing", "version": "1.0"}, + "name": "gostproxy", + "guid": "pkg-aaaaaaaaaaaaaaaaaaaa", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "creatorGUID": "test-creator-guid", + "labels": {"app": "gostproxy"}, + "createdAt": "2025-09-22T07:30:13Z", + "updatedAt": "2025-09-22T07:30:13Z", + "version": "v1.0.0", + "description": "", }, "spec": { - "users": [{"emailID": "test.user@example.com", "role": "admin"}], - "features": {"vpn": {"enabled": False}}, + "runtime": "cloud", + "executables": [ + { + "name": "gostproxy", + "type": "docker", + "docker": { + "imagePullPolicy": "IfNotPresent", + "image": "gostproxy:v1.0.0", + "pullSecret": {"depends": {}}, + }, + "limits": {"cpu": 0.25, "memory": 128}, + } + ], + "environmentVars": [ + { + "name": "DEVICE_NAME", + "description": "Device Name in Tailscale", + } + ], + "ros": {}, + "endpoints": [ + { + "name": "gateway", + "type": "external-https", + "port": 443, + "targetPort": 80, + } + ], + "cloud": {"replicas": 1}, }, } @pytest.fixture -def package_body(): +def device_package_model_mock(): return { "apiVersion": "apiextensions.rapyuta.io/v1", "kind": "Package", "metadata": { - "name": "test-package", + "name": "database", + "guid": "pkg-bbbbbbbbbbbbbbbbbbbb", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "creatorGUID": "test-creator-guid", + "labels": {"app": "database"}, + "createdAt": "2025-09-22T07:12:54Z", + "updatedAt": "2025-09-22T07:12:54Z", "version": "v1.0.0", - "description": "Test package for demo", - "labels": {"app": "test"}, - "projectguid": "mock_project_guid", + "description": ( + "Database package for deploying postgres and postgres_exporter" + ), }, - "spec": {"runtime": "cloud", "cloud": {"enabled": True}}, + "spec": { + "runtime": "device", + "executables": [ + { + "name": "postgres", + "type": "docker", + "docker": { + "imagePullPolicy": "IfNotPresent", + "image": "postgis:16-3.4", + "pullSecret": { + "depends": {"kind": "secret", "nameOrGUID": "mock-secret"} + }, + }, + } + ], + "environmentVars": [ + { + "name": "POSTGRES_MULTIPLE_DATABASES", + "default": "test_table, test_table2", + } + ], + "ros": {}, + "device": {"arch": "amd64", "restart": "always"}, + }, + } + + +@pytest.fixture +def packagelist_model_mock(cloud_package_model_mock, device_package_model_mock): + return { + "metadata": { + "continue": 1, + }, + "items": [cloud_package_model_mock, device_package_model_mock], } +# -------------------- DEPLOYMENT -------------------- + + @pytest.fixture def deployment_body(): + # Updated to match device_deployment_model_mock keys and values, but only using keys present in deployment_body return { "apiVersion": "apiextensions.rapyuta.io/v1", "kind": "Deployment", "metadata": { - "name": "test-deployment", + "name": "device_deployment_sample", + "depends": { + "kind": "package", + "nameOrGUID": "device-package", + "version": "2.0.0", + }, + "labels": {"app": "deviceapp"}, + }, + "spec": { + "runtime": "device", + "device": {"depends": {"kind": "device", "nameOrGUID": "device-sample-001"}}, + "restart": "always", + "envArgs": [ + {"name": "DEVICE_ENV", "value": "true"}, + {"name": "DEVICE_ID", "value": "dev-001"}, + {"name": "DEVICE_SECRET", "value": "secret"}, + ], + }, + } + + +@pytest.fixture +def cloud_deployment_model_mock(): + return { + "kind": "Deployment", + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "name": "cloud_deployment_sample", + "guid": "dep-cloud-001", + "projectGUID": "project-sample-001", + "organizationGUID": "org-sample-001", + "creatorGUID": "user-sample-001", + "createdAt": "2025-01-01T10:00:00Z", + "updatedAt": "2025-01-01T11:00:00Z", + "deletedAt": None, + "organizationName": "Sample Org", + "projectName": "Sample Project", + "depends": { + "kind": "package", + "nameOrGUID": "cloud-package", + "version": "1.0.0", + }, + "generation": 1, + "labels": {"app": "cloudapp"}, + "region": "us", + }, + "spec": { + "runtime": "cloud", + "envArgs": [ + {"name": "CLOUD_ENV", "value": "true"}, + {"name": "API_KEY", "value": "cloudapikey"}, + { + "name": "CLOUD_ENDPOINT", + "value": "cloud.example.com", + "exposed": True, + "exposedName": "CLOUD_ENDPOINT", + }, + ], + "features": {"vpn": {"enabled": True}}, + "staticRoutes": [ + { + "name": "cloudroute", + "url": "cloudroute.example.com", + "depends": {"kind": "staticroute", "nameOrGUID": "cloudroute-sample"}, + } + ], + }, + "status": { + "status": "Running", + "phase": "Succeeded", + "executables_status": { + "cloud_exec": { + "name": "cloud_exec", + "status": "running", + "reason": "CloudRunning", + } + }, + "dependencies": {}, + }, + } + + +@pytest.fixture +def device_deployment_model_mock(): + return { + "kind": "Deployment", + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "name": "device_deployment_sample", + "guid": "dep-device-001", + "projectGUID": "project-sample-001", + "organizationGUID": "org-sample-001", + "creatorGUID": "user-sample-001", + "createdAt": "2025-01-02T10:00:00Z", + "updatedAt": "2025-01-02T11:00:00Z", + "deletedAt": None, + "organizationName": "Sample Org", + "projectName": "Sample Project", "depends": { - "kind": "Package", - "nameOrGUID": "mock_package_guid", + "kind": "package", + "nameOrGUID": "device-package", + "version": "2.0.0", }, + "generation": 1, + "labels": {"app": "deviceapp"}, }, - "restart": "Always", + "spec": { + "runtime": "device", + "envArgs": [ + {"name": "DEVICE_ENV", "value": "true"}, + {"name": "DEVICE_ID", "value": "dev-001"}, + {"name": "DEVICE_SECRET", "value": "secret"}, + ], + "volumes": [ + { + "execName": "device_exec", + "mountPath": "/mnt/data", + "subPath": "/mnt/data", + "depends": {}, + } + ], + "features": {}, + "device": {"depends": {"kind": "device", "nameOrGUID": "device-sample-001"}}, + }, + "status": { + "status": "Running", + "phase": "Succeeded", + "executables_status": { + "device_exec": { + "name": "device_exec", + "status": "running", + "reason": "DeviceRunning", + } + }, + "dependencies": {}, + }, + } + + +@pytest.fixture +def deploymentlist_model_mock(cloud_deployment_model_mock, device_deployment_model_mock): + return { + "metadata": { + "continue": 123, + }, + "items": [cloud_deployment_model_mock, device_deployment_model_mock], } +# -------------------- DISK -------------------- + + @pytest.fixture def disk_body(): return { @@ -165,6 +363,113 @@ def disk_body(): } +@pytest.fixture +def disk_model_mock(): + return { + "kind": "Disk", + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "name": "mock_disk_1", + "guid": "disk-mockdisk123456789101", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "organizationGUID": "org-mock-789", + "creatorGUID": "mock-user-guid-000", + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2025-01-01T01:00:00Z", + "deletedAt": None, + "organizationName": "Mock Org", + "projectName": "Mock Project", + "generation": 1, + }, + "spec": { + "runtime": "cloud", + "capacity": "4", + }, + "status": { + "status": "Available", + }, + } + + +@pytest.fixture +def disklist_model_mock(disk_model_mock): + return { + "metadata": { + "continue": 1, + }, + "items": [disk_model_mock], + } + + +# -------------------- SECRET -------------------- + + +@pytest.fixture +def secret_body(): + return { + "apiVersion": "apiextensions.rapyuta.io/v1", + "kind": "Secret", + "metadata": { + "name": "test_secret", + "labels": {"app": "test"}, + }, + "spec": { + "type": "Docker", + "docker": { + "username": "test-user", + "password": "test-password", + "email": "test@email.com", + "registry": "https://index.docker.io/v1/", + }, + }, + } + + +@pytest.fixture +def secret_model_mock(): + return { + "apiVersion": "api.rapyuta.io/v2", + "kind": "Secret", + "metadata": { + "createdAt": "2025-01-01T00:00:00Z", + "creatorGUID": "mock-user-guid-000", + "deletedAt": None, + "guid": "secret-aaaaaaaaaaaaaaaaaaaa", + "labels": {"app": "test"}, + "name": "test_secret", + "organizationCreatorGUID": "mock-user-guid-000", + "organizationGUID": "org-mock-789", + "organizationName": "Mock Org", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", # <-- valid project GUID + "projectName": "Mock Project", + "region": "jp", + "shortGUID": "abcde", + "updatedAt": "2025-01-01T01:00:00Z", + }, + "spec": { + "docker": { + "email": "test@example.com", + "password": "password", + "registry": "docker.io", + "username": "testuser", + } + }, + } + + +@pytest.fixture +def secretlist_model_mock(secret_model_mock): + return { + "metadata": { + "continue": 1, + }, + "items": [secret_model_mock], + } + + +# -------------------- STATIC ROUTE -------------------- + + @pytest.fixture def staticroute_body(): return { @@ -178,6 +483,49 @@ def staticroute_body(): } +@pytest.fixture +def staticroute_model_mock(): + return { + "kind": "StaticRoute", + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "createdAt": "2025-01-01T00:00:00Z", + "creatorGUID": "mock-user-guid-000", + "deletedAt": None, + "guid": "staticroute-aaaaaaaaaaaaaaaaaaaa", + "labels": {"app": "test"}, + "name": "test-staticroute", + "organizationCreatorGUID": "mock-user-guid-000", + "organizationGUID": "org-mock-789", + "organizationName": "Mock Org", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "projectName": "Mock Project", + "region": "jp", + "shortGUID": "abcde", + "updatedAt": "2025-01-01T01:00:00Z", + }, + "spec": {"sourceIPRange": ["10.0.0.0/24"], "url": "https://example.com"}, + "status": { + "deploymentID": "deployment-123", + "packageID": "package-123", + "status": "Available", + }, + } + + +@pytest.fixture +def staticroutelist_model_mock(staticroute_model_mock): + return { + "metadata": { + "continue": 1, + }, + "items": [staticroute_model_mock], + } + + +# -------------------- NETWORK -------------------- + + @pytest.fixture def network_body(): return { @@ -188,21 +536,64 @@ def network_body(): "region": "jp", "labels": {"app": "test"}, }, + "spec": { + "rosDistro": "kinetic", + "runtime": "cloud", + "type": "routed", + }, } @pytest.fixture -def secret_body(): +def network_model_mock(): return { "apiVersion": "apiextensions.rapyuta.io/v1", - "kind": "Secret", + "kind": "Network", "metadata": { - "name": "test-secret", + "createdAt": "2025-09-22T07:00:00Z", + "creatorGUID": "mock-user-guid-000", + "deletedAt": None, + "guid": "network-aaaaaaaaaaaaaaaaaaaa", "labels": {"app": "test"}, + "name": "test-network", + "organizationCreatorGUID": "mock-user-guid-000", + "organizationGUID": "org-mock-789", + "organizationName": "Mock Org", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "projectName": "Mock Project", + "region": "jp", + "shortGUID": "abcde", + "updatedAt": "2025-09-22T07:10:00Z", + }, + "spec": { + "architecture": "amd64", + "depends": {"kind": "Device", "nameOrGUID": "device-aaaaaaaaaaaaaaaaaaaa"}, + "discoveryServer": {"serverID": 1, "serverPort": 11311}, + "networkInterface": "eth0", + "rabbitMQCreds": {"defaultPassword": "guest", "defaultUser": "guest"}, + "resourceLimits": {"cpu": 0.05, "memory": 256}, + "restartPolicy": "always", + "rosDistro": "kinetic", + "runtime": "cloud", + "type": "routed", + }, + "status": {"errorCodes": [], "phase": "InProgress", "status": "Running"}, + } + + +@pytest.fixture +def networklist_model_mock(network_model_mock): + return { + "metadata": { + "continue": 1, }, + "items": [network_model_mock], } +# -------------------- CONFIG TREE -------------------- + + @pytest.fixture def configtree_body(): return { @@ -215,8 +606,209 @@ def configtree_body(): } +# -------------------- USER -------------------- + + +@pytest.fixture +def mock_response_user(): + return { + "kind": "User", + "metadata": {"name": "test user", "guid": "mock_user_guid"}, + "spec": { + "emailID": "test.user@example.com", + "firstName": "Test", + "lastName": "User", + "userGUID": "mock_user_guid", + "role": "admin", + "organizations": [ + { + "guid": "mock_org_guid", + "name": "mock-org", + "shortGUID": "org123", + "creator": "mock_user_guid", + } + ], + "projects": [ + { + "guid": "mock_project_guid", + "name": "mock-project", + "organizationGUID": "mock_org_guid", + "creator": "mock_user_guid", + } + ], + "userGroupsMembers": [ + { + "guid": "mock_group_guid", + "name": "mock-group", + "organizationGUID": "mock_org_guid", + "role": "member", + } + ], + "userGroupAdmins": [ + { + "guid": "mock_admin_group_guid", + "name": "mock-admin-group", + "organizationGUID": "mock_org_guid", + "role": "admin", + } + ], + }, + } + + +@pytest.fixture +def user_body(): + return { + "emailID": "test.user@example.com", + "firstName": "Test", + "lastName": "User", + "userGUID": "mock_user_guid", + "role": "admin", + } + + +# -------------------- ORGANIZATION -------------------- + + +@pytest.fixture +def mock_response_organization(): + return { + "metadata": { + "name": "test-org", + "guid": "mock_org_guid", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "organizationGUID": "org-mock-789", + "creatorGUID": "mock-user-guid-000", + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2025-01-01T01:00:00Z", + "deletedAt": None, + "organizationName": "Mock Org", + "projectName": "Mock Project", + }, + "spec": { + "users": [ + { + "guid": "mock_user1_guid", + "firstName": "John", + "lastName": "Doe", + "emailID": "test.user1@rapyuta-robotics.com", + "roleInOrganization": "viewer", + }, + { + "guid": "mock_user2_guid", + "firstName": "Jane", + "lastName": "Smith", + "emailID": "test.user2@rapyuta-robotics.com", + "roleInOrganization": "admin", + }, + ] + }, + } + + +@pytest.fixture +def organization_body(): + return { + "metadata": { + "name": "test-org", + "guid": "mock_org_guid", + }, + "spec": { + "users": [ + { + "guid": "mock_user1_guid", + "firstName": "John", + "lastName": "Doe", + "emailID": "test.user1@rapyuta-robotics.com", + "roleInOrganization": "viewer", + }, + { + "guid": "mock_user2_guid", + "firstName": "Jane", + "lastName": "Smith", + "emailID": "test.user2@rapyuta-robotics.com", + "roleInOrganization": "admin", + }, + ] + }, + } + + +# -------------------- MANAGED SERVICE -------------------- + + +@pytest.fixture +def managedservice_model_mock(): + return { + "apiVersion": "api.rapyuta.io/v2", + "kind": "ManagedServiceInstance", + "metadata": { + "guid": "mock_instance_guid", + "name": "test-instance", + "creatorGUID": "creator-guid", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "labels": {"env": "test"}, + }, + "spec": { + "provider": "elasticsearch", + "config": {"version": "7.10", "nodes": 3, "storage": "100Gi"}, + }, + } + + +@pytest.fixture +def managedservicelist_model_mock(managedservice_model_mock): + return { + "metadata": { + "continue": 1, + }, + "items": [managedservice_model_mock], + } + + +@pytest.fixture +def managedservicebindinglist_model_mock(managedservice_binding_model_mock): + return { + "metadata": { + "continue": 1, + }, + "items": [managedservice_binding_model_mock], + } + + +@pytest.fixture +def managedservice_binding_model_mock(): + return { + "apiVersion": "api.rapyuta.io/v2", + "kind": "ManagedServiceBinding", + "metadata": { + "guid": "mock_instance_binding_guid", + "name": "test-instance-binding", + "creatorGUID": "creator-guid", + "projectGUID": "project-aaaaaaaaaaaaaaaaaaaa", + "labels": {"env": "test"}, + }, + "spec": { + "provider": "headscalevpn", + "config": {"version": "1.0"}, + }, + } + + +# -------------------- CONFIGURATION -------------------- + + @pytest.fixture def mock_config(): + return { + "project_id": "mock_project_guid", + "organization_id": "mock_org_guid", + "auth_token": "mock_auth_token", + } + + +@pytest.fixture +def config_obj(): return Configuration( project_guid="mock_project_guid", organization_guid="mock_org_guid", diff --git a/tests/sync_tests/test_config.py b/tests/sync_tests/test_config.py index 3d5edf7..62d8d12 100644 --- a/tests/sync_tests/test_config.py +++ b/tests/sync_tests/test_config.py @@ -1,16 +1,14 @@ import json + +# ruff: noqa: F811, F401 + +import pytest from rapyuta_io_sdk_v2.config import Configuration -from tests.data.mock_data import mock_config # noqa: F401 +from tests.data.mock_data import mock_config, config_obj -def test_from_file(mocker): - # Mock configuration file content - mock_config_data = { - "project_id": "mock_project_guid", - "organization_id": "mock_org_guid", - "auth_token": "mock_auth_token", - } - mock_file_content = json.dumps(mock_config_data) +def test_from_file(mocker, mock_config): + mock_file_content = json.dumps(mock_config) # Mock the open function mocker.patch("builtins.open", mocker.mock_open(read_data=mock_file_content)) @@ -24,14 +22,14 @@ def test_from_file(mocker): config = Configuration.from_file(file_path="/mock/path/to/config.json") # Assert the Configuration object contains the expected values - assert config.project_guid == mock_config_data["project_id"] - assert config.organization_guid == mock_config_data["organization_id"] - assert config.auth_token == mock_config_data["auth_token"] + assert config.project_guid == mock_config["project_id"] + assert config.organization_guid == mock_config["organization_id"] + assert config.auth_token == mock_config["auth_token"] -def test_get_headers_basic(mock_config): # noqa: F811 +def test_get_headers_basic(config_obj): # Call the method without passing any arguments - headers = mock_config.get_headers() + headers = config_obj.get_headers() # Verify the headers assert headers["Authorization"] == "Bearer mock_auth_token" @@ -39,9 +37,9 @@ def test_get_headers_basic(mock_config): # noqa: F811 assert headers["project"] == "mock_project_guid" -def test_get_headers_without_project(mock_config): # noqa: F811 +def test_get_headers_without_project(config_obj): # Call the method with `with_project=False` - headers = mock_config.get_headers(with_project=False) + headers = config_obj.get_headers(with_project=False) # Verify the headers assert headers["Authorization"] == "Bearer mock_auth_token" @@ -49,9 +47,9 @@ def test_get_headers_without_project(mock_config): # noqa: F811 assert "project" not in headers -def test_get_headers_with_custom_values(mock_config): # noqa: F811 +def test_get_headers_with_custom_values(config_obj): # Call the method with custom organization_guid and project_guid - headers = mock_config.get_headers( + headers = config_obj.get_headers( organization_guid="custom_org_guid", project_guid="custom_project_guid", ) @@ -62,12 +60,12 @@ def test_get_headers_with_custom_values(mock_config): # noqa: F811 assert headers["project"] == "custom_project_guid" -def test_get_headers_with_request_id(mocker, mock_config): # noqa: F811 +def test_get_headers_with_request_id(mocker, config_obj): # Mock the environment variable mocker.patch("os.getenv", return_value="mock_request_id") # Call the method - headers = mock_config.get_headers() + headers = config_obj.get_headers() # Verify the headers assert headers["Authorization"] == "Bearer mock_auth_token" diff --git a/tests/sync_tests/test_configtree.py b/tests/sync_tests/test_configtree.py index 3d2f27a..abd5d55 100644 --- a/tests/sync_tests/test_configtree.py +++ b/tests/sync_tests/test_configtree.py @@ -1,17 +1,14 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import configtree_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from tests.data.mock_data import configtree_body +from tests.utils.fixtures import client -def test_list_configtrees_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method +def test_list_configtrees_success(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, json={ @@ -19,158 +16,99 @@ def test_list_configtrees_success(client, mocker: MockFixture): # noqa: F811 "items": [{"name": "test-configtree", "guid": "mock_configtree_guid"}], }, ) - - # Call the list_configtrees method response = client.list_configtrees() - - # Validate the response - assert isinstance(response, Munch) assert response["items"] == [ {"name": "test-configtree", "guid": "mock_configtree_guid"} ] -def test_list_configtrees_bad_gateway(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method +def test_list_configtrees_bad_gateway(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=502, json={"error": "bad gateway"}, ) - - # Call the list_configtrees method with pytest.raises(Exception) as exc: client.list_configtrees() - assert str(exc.value) == "bad gateway" -def test_create_configtree_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.post method +def test_create_configtree_success(client, mocker: MockFixture): mock_post = mocker.patch("httpx.Client.post") - - # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, json={ "metadata": {"guid": "test_configtree_guid", "name": "test_configtree"}, }, ) - - # Call the create_configtree method response = client.create_configtree(configtree_body) - - # Validate the response - assert isinstance(response, Munch) assert response["metadata"]["guid"] == "test_configtree_guid" -def test_create_configtree_service_unavailable(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.post method +def test_create_configtree_service_unavailable(client, mocker: MockFixture): mock_post = mocker.patch("httpx.Client.post") - - # Set up the mock response mock_post.return_value = httpx.Response( status_code=503, json={"error": "service unavailable"}, ) - - # Call the create_configtree method with pytest.raises(Exception) as exc: client.create_configtree(configtree_body) - assert str(exc.value) == "service unavailable" -def test_get_configtree_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method +def test_get_configtree_success(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_configtree_guid", "name": "test_configtree"}, }, ) - - # Call the get_configtree method response = client.get_configtree(name="mock_configtree_name") - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_configtree_guid" - assert response.metadata.name == "test_configtree" + assert response["metadata"]["guid"] == "test_configtree_guid" + assert response["metadata"]["name"] == "test_configtree" -def test_set_configtree_revision_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.put method +def test_set_configtree_revision_success(client, mocker: MockFixture): mock_put = mocker.patch("httpx.Client.put") - - # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_configtree_guid", "name": "test_configtree"}, }, ) - - # Call the set_configtree_revision method response = client.set_configtree_revision( name="mock_configtree_name", configtree=configtree_body ) - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_configtree_guid" - assert response.metadata.name == "test_configtree" + assert response["metadata"]["guid"] == "test_configtree_guid" + assert response["metadata"]["name"] == "test_configtree" -def test_update_configtree_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.put method +def test_update_configtree_success(client, mocker: MockFixture): mock_put = mocker.patch("httpx.Client.put") - - # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_configtree_guid", "name": "test_configtree"}, }, ) - - # Call the update_configtree method response = client.update_configtree(name="mock_configtree_name", body=configtree_body) - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_configtree_guid" - assert response.metadata.name == "test_configtree" + assert response["metadata"]["guid"] == "test_configtree_guid" + assert response["metadata"]["name"] == "test_configtree" -def test_delete_configtree_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.delete method +def test_delete_configtree_success(client, mocker: MockFixture): mock_delete = mocker.patch("httpx.Client.delete") - - # Set up the mock response mock_delete.return_value = httpx.Response( status_code=204, json={"success": True}, ) - - # Call the delete_configtree method response = client.delete_configtree(name="mock_configtree_name") - - # Validate the response - assert response["success"] is True + assert response is None -def test_list_revisions_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method +def test_list_revisions_success(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, json={ @@ -178,164 +116,112 @@ def test_list_revisions_success(client, mocker: MockFixture): # noqa: F811 "items": [{"name": "test-configtree", "guid": "mock_configtree_guid"}], }, ) - - # Call the list_revisions method response = client.list_revisions(tree_name="mock_configtree_name") - - # Validate the response - assert isinstance(response, Munch) assert response["items"] == [ {"name": "test-configtree", "guid": "mock_configtree_guid"} ] -def test_create_revision_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.post method +def test_create_revision_success(client, mocker: MockFixture): mock_post = mocker.patch("httpx.Client.post") - - # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, json={ "metadata": {"guid": "test_revision_guid", "name": "test_revision"}, }, ) - - # Call the create_revision method response = client.create_revision(name="mock_configtree_name", body=configtree_body) - - # Validate the response - assert isinstance(response, Munch) assert response["metadata"]["guid"] == "test_revision_guid" -def test_put_keys_in_revision_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.put method +def test_put_keys_in_revision_success(client, mocker: MockFixture): mock_put = mocker.patch("httpx.Client.put") - - # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_revision_guid", "name": "test_revision"}, }, ) - - # Call the put_keys_in_revision method response = client.put_keys_in_revision( name="mock_configtree_name", revision_id="mock_revision_id", config_values=["mock_value1", "mock_value2"], ) - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" -def test_commit_revision_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.put method +def test_commit_revision_success(client, mocker: MockFixture): mock_patch = mocker.patch("httpx.Client.patch") - - # Set up the mock response mock_patch.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_revision_guid", "name": "test_revision"}, }, ) - - # Call the commit_revision method response = client.commit_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", ) - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" -def test_get_key_in_revision(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method +def test_get_key_in_revision(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_revision_guid", "name": "test_revision"}, }, ) - - # Call the get_key_in_revision method response = client.get_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key" ) - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" -def test_put_key_in_revision_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.put method +def test_put_key_in_revision_success(client, mocker: MockFixture): mock_put = mocker.patch("httpx.Client.put") - - # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_revision_guid", "name": "test_revision"}, }, ) - - # Call the put_key_in_revision method response = client.put_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key" ) - - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" -def test_delete_key_in_revision_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_key_in_revision_success(client, mocker: MockFixture): mock_delete = mocker.patch("httpx.Client.delete") - mock_delete.return_value = httpx.Response( status_code=204, json={"success": True}, ) - response = client.delete_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key" ) + assert response is None - assert response["success"] is True - -def test_rename_key_in_revision_success(client, mocker: MockFixture): # noqa: F811 +def test_rename_key_in_revision_success(client, mocker: MockFixture): mock_patch = mocker.patch("httpx.Client.patch") - mock_patch.return_value = httpx.Response( status_code=200, json={ "metadata": {"guid": "test_revision_guid", "name": "test_revision"}, }, ) - response = client.rename_key_in_revision( tree_name="mock_configtree_name", revision_id="mock_revision_id", key="mock_key", config_key_rename={"metadata": {"name": "test_key"}}, ) - - assert isinstance(response, Munch) - assert response.metadata.guid == "test_revision_guid" - assert response.metadata.name == "test_revision" + assert response["metadata"]["guid"] == "test_revision_guid" + assert response["metadata"]["name"] == "test_revision" diff --git a/tests/sync_tests/test_deployment.py b/tests/sync_tests/test_deployment.py index 3e30af4..ba9faee 100644 --- a/tests/sync_tests/test_deployment.py +++ b/tests/sync_tests/test_deployment.py @@ -1,36 +1,40 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import deployment_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import DeploymentList, Deployment +from tests.utils.fixtures import client +from tests.data import ( + deployment_body, + deploymentlist_model_mock, + cloud_deployment_model_mock, + device_deployment_model_mock, +) -def test_list_deployments_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method +def test_list_deployments_success(client, deploymentlist_model_mock, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock responses for pagination + # Use the DeploymentList pydantic model mock and dump as JSON mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-deployment", "guid": "mock_deployment_guid"}], - }, + json=deploymentlist_model_mock, ) - # Call the list_deployments method response = client.list_deployments() - # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [ - {"name": "test-deployment", "guid": "mock_deployment_guid"} - ] + assert isinstance(response, DeploymentList) + assert response.metadata.continue_ == 123 + assert len(response.items) == 2 + cloud_dep = response.items[0] + device_dep = response.items[1] + assert cloud_dep.spec.runtime == "cloud" + assert device_dep.spec.runtime == "device" + assert cloud_dep.metadata.guid == "dep-cloud-001" + assert device_dep.metadata.guid == "dep-device-001" -def test_list_deployments_not_found(client, mocker: MockFixture): # noqa: F811 +def test_list_deployments_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -46,28 +50,35 @@ def test_list_deployments_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "not found" -def test_get_deployment_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method +def test_get_cloud_deployment_success( + client, cloud_deployment_model_mock, mocker: MockFixture +): mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Deployment", - "metadata": {"guid": "test_deployment_guid", "name": "test_deployment"}, - }, + json=cloud_deployment_model_mock, ) + response = client.get_deployment(name="cloud_deployment_sample") + assert isinstance(response, Deployment) + assert response.spec.runtime == "cloud" + assert response.metadata.guid == "dep-cloud-001" - # Call the get_deployment method - response = client.get_deployment(name="mock_deployment_name") - # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_deployment_guid" +def test_get_device_deployment_success( + client, device_deployment_model_mock, mocker: MockFixture +): + mock_get = mocker.patch("httpx.Client.get") + mock_get.return_value = httpx.Response( + status_code=200, + json=device_deployment_model_mock, + ) + response = client.get_deployment(name="device_deployment_sample") + assert isinstance(response, Deployment) + assert response.spec.runtime == "device" + assert response.metadata.guid == "dep-device-001" -def test_get_deployment_not_found(client, mocker: MockFixture): # noqa: F811 +def test_get_deployment_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -84,24 +95,21 @@ def test_get_deployment_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "deployment not found" -def test_create_deployment_success(client, deployment_body, mocker: MockFixture): # noqa: F811 +def test_create_deployment_unauthorized(client, deployment_body, mocker: MockFixture): mock_post = mocker.patch("httpx.Client.post") mock_post.return_value = httpx.Response( - status_code=200, - json={ - "kind": "Deployment", - "metadata": {"guid": "test_deployment_guid", "name": "test_deployment"}, - }, + status_code=401, + json={"error": "unauthorized"}, ) - response = client.create_deployment(body=deployment_body) + with pytest.raises(Exception) as exc: + client.create_deployment(body=deployment_body) - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_deployment_guid" + assert str(exc.value) == "unauthorized" -def test_create_deployment_unauthorized(client, deployment_body, mocker: MockFixture): # noqa: F811 +def test_create_deployment_unauthorized(client, deployment_body, mocker: MockFixture): mock_post = mocker.patch("httpx.Client.post") mock_post.return_value = httpx.Response( @@ -115,28 +123,29 @@ def test_create_deployment_unauthorized(client, deployment_body, mocker: MockFix assert str(exc.value) == "unauthorized" -def test_update_deployment_success(client, deployment_body, mocker: MockFixture): # noqa: F811 +def test_update_deployment_success( + client, deployment_body, device_deployment_model_mock, mocker: MockFixture +): mock_put = mocker.patch("httpx.Client.put") mock_put.return_value = httpx.Response( status_code=200, - json={ - "kind": "Deployment", - "metadata": {"guid": "test_deployment_guid", "name": "test_deployment"}, - }, + json=device_deployment_model_mock, ) - response = client.update_deployment(name="mock_deployment_name", body=deployment_body) + response = client.update_deployment( + name="device_deployment_sample", body=deployment_body + ) - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_deployment_guid" + assert isinstance(response, Deployment) + assert response.metadata.guid == "dep-device-001" -def test_delete_deployment_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_deployment_success(client, mocker: MockFixture): mock_delete = mocker.patch("httpx.Client.delete") mock_delete.return_value = httpx.Response(status_code=204, json={"success": True}) response = client.delete_deployment(name="mock_deployment_name") - assert response["success"] is True + assert response is None diff --git a/tests/sync_tests/test_disk.py b/tests/sync_tests/test_disk.py index cb9e01e..da16c9d 100644 --- a/tests/sync_tests/test_disk.py +++ b/tests/sync_tests/test_disk.py @@ -1,34 +1,37 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import disk_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import Disk, DiskList +from tests.data.mock_data import disk_body, disk_model_mock, disklist_model_mock +from tests.utils.fixtures import client -def test_list_disks_success(client, mocker: MockFixture): # noqa: F811 +def test_list_disks_success(client, disklist_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-disk", "guid": "mock_disk_guid"}], - }, + json=disklist_model_mock, ) # Call the list_disks method response = client.list_disks() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-disk", "guid": "mock_disk_guid"}] + assert isinstance(response, DiskList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + disk = response.items[0] + assert disk.metadata.guid == "disk-mockdisk123456789101" + assert disk.metadata.name == "mock_disk_1" + assert disk.kind == "Disk" -def test_list_disks_not_found(client, mocker: MockFixture): # noqa: F811 +def test_list_disks_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -44,28 +47,26 @@ def test_list_disks_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "not found" -def test_get_disk_success(client, mocker: MockFixture): # noqa: F811 +def test_get_disk_success(client, disk_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Disk", - "metadata": {"guid": "test_disk_guid", "name": "mock_disk_name"}, - }, + json=disk_model_mock, ) # Call the get_disk method response = client.get_disk(name="mock_disk_name") # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_disk_guid" + assert isinstance(response, Disk) + assert response.metadata.guid == "disk-mockdisk123456789101" + assert response.metadata.name == "mock_disk_1" -def test_get_disk_not_found(client, mocker: MockFixture): # noqa: F811 +def test_get_disk_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -82,25 +83,22 @@ def test_get_disk_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "disk not found" -def test_create_disk_success(client, disk_body, mocker: MockFixture): # noqa: F811 +def test_create_disk_success(client, disk_body, disk_model_mock, mocker: MockFixture): mock_post = mocker.patch("httpx.Client.post") mock_post.return_value = httpx.Response( status_code=200, - json={ - "kind": "Disk", - "metadata": {"guid": "test_disk_guid", "name": "test_disk"}, - }, + json=disk_model_mock, ) response = client.create_disk(body=disk_body, project_guid="mock_project_guid") - assert isinstance(response, Munch) - assert response.metadata.guid == "test_disk_guid" - assert response.metadata.name == "test_disk" + assert isinstance(response, Disk) + assert response.metadata.guid == "disk-mockdisk123456789101" + assert response.metadata.name == "mock_disk_1" -def test_delete_disk_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_disk_success(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") @@ -114,10 +112,10 @@ def test_delete_disk_success(client, mocker: MockFixture): # noqa: F811 response = client.delete_disk(name="mock_disk_name") # Validate the response - assert response["success"] is True + assert response is None -def test_delete_disk_not_found(client, mocker: MockFixture): # noqa: F811 +def test_delete_disk_not_found(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") diff --git a/tests/sync_tests/test_main.py b/tests/sync_tests/test_main.py index b906dae..5bedcf3 100644 --- a/tests/sync_tests/test_main.py +++ b/tests/sync_tests/test_main.py @@ -2,10 +2,11 @@ import pytest from pytest_mock import MockFixture -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from tests.utils.fixtures import client -def test_get_auth_token_success(client, mocker: MockFixture): # noqa: F811 +def test_get_auth_token_success(client, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") @@ -26,7 +27,7 @@ def test_get_auth_token_success(client, mocker: MockFixture): # noqa: F811 assert response == "mock_token" -def test_login_success(client, mocker: MockFixture): # noqa: F811 +def test_login_success(client, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") @@ -42,7 +43,7 @@ def test_login_success(client, mocker: MockFixture): # noqa: F811 assert client.config.auth_token == "mock_token_2" -def test_login_failure(client, mocker: MockFixture): # noqa: F811 +def test_login_failure(client, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") diff --git a/tests/sync_tests/test_managedservice.py b/tests/sync_tests/test_managedservice.py index cdd08d3..909cc7f 100644 --- a/tests/sync_tests/test_managedservice.py +++ b/tests/sync_tests/test_managedservice.py @@ -1,12 +1,25 @@ import httpx -import pytest # noqa: F401 -from munch import Munch from pytest_mock import MockFixture -from tests.utils.fixtures import client # noqa: F401 - - -def test_list_providers_success(client, mocker: MockFixture): # noqa: F811 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import ( + ManagedServiceBinding, + ManagedServiceInstanceList, + ManagedServiceBindingList, + ManagedServiceInstance, + ManagedServiceProvider, + ManagedServiceProviderList, +) +from tests.utils.fixtures import client +from tests.data import ( + managedservice_binding_model_mock, + managedservice_model_mock, + managedservicebindinglist_model_mock, + managedservicelist_model_mock, +) + + +def test_list_providers_success(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -23,72 +36,88 @@ def test_list_providers_success(client, mocker: MockFixture): # noqa: F811 response = client.list_providers() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-provider", "guid": "mock_provider_guid"}] + assert isinstance(response, ManagedServiceProviderList) + assert isinstance(response.items[0], ManagedServiceProvider) + assert response.items[0].name == "test-provider" -def test_list_instances_success(client, mocker: MockFixture): # noqa: F811 +def test_list_instances_success( + client, managedservicelist_model_mock, mocker: MockFixture +): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-instance", "guid": "mock_instance_guid"}], - }, + json=managedservicelist_model_mock, ) # Call the list_instances method response = client.list_instances() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-instance", "guid": "mock_instance_guid"}] - - -def test_get_instance_success(client, mocker: MockFixture): # noqa: F811 + assert isinstance(response, ManagedServiceInstanceList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + instance = response.items[0] + assert instance.metadata.guid == "mock_instance_guid" + assert instance.metadata.name == "test-instance" + assert instance.kind == "ManagedServiceInstance" + assert instance.spec.provider == "elasticsearch" + assert instance.spec.config["version"] == "7.10" + + +def test_get_instance_success(client, managedservice_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_instance_guid", "name": "test_instance"}, - }, + json=managedservice_model_mock, ) # Call the get_instance method response = client.get_instance(name="mock_instance_name") # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_guid" + assert isinstance(response, ManagedServiceInstance) + assert response.metadata.guid == "mock_instance_guid" + assert response.metadata.name == "test-instance" + assert response.kind == "ManagedServiceInstance" + assert response.spec.provider == "elasticsearch" -def test_create_instance_success(client, mocker: MockFixture): # noqa: F811 +def test_create_instance_success(client, managedservice_model_mock, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": {"guid": "test_instance_guid", "name": "test_instance"}, - }, + json=managedservice_model_mock, ) # Call the create_instance method - response = client.create_instance(body={"name": "test_instance"}) + # print(ManagedServiceInstance.model_json_schema()) + response = client.create_instance( + body={ + "apiVersion": "api.rapyuta.io/v2", + "metadata": { + "name": "test-instance", + }, + } + ) - # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_guid" + # # Validate the response + assert isinstance(response, ManagedServiceInstance) + assert response.metadata.guid == "mock_instance_guid" + assert response.metadata.name == "test-instance" + assert response.kind == "ManagedServiceInstance" -def test_delete_instance_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_instance_success(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") @@ -102,47 +131,45 @@ def test_delete_instance_success(client, mocker: MockFixture): # noqa: F811 response = client.delete_instance(name="mock_instance_name") # Validate the response - assert response["success"] is True + assert response is None -def test_list_instance_bindings_success(client, mocker: MockFixture): # noqa: F811 +def test_list_instance_bindings_success( + client, managedservicebindinglist_model_mock, mocker: MockFixture +): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [ - {"name": "test-instance-binding", "guid": "mock_instance_binding_guid"} - ], - }, + json=managedservicebindinglist_model_mock, ) # Call the list_instance_bindings method - response = client.list_instance_bindings("mock_instance_name") + response = client.list_instance_bindings(instance_name="mock_instance_name") # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [ - {"name": "test-instance-binding", "guid": "mock_instance_binding_guid"} - ] - - -def test_get_instance_binding_success(client, mocker: MockFixture): # noqa: F811 + assert isinstance(response, ManagedServiceBindingList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + binding = response.items[0] + assert binding.metadata.guid == "mock_instance_binding_guid" + assert binding.metadata.name == "test-instance-binding" + assert binding.kind == "ManagedServiceBinding" + assert binding.spec.provider == "headscalevpn" + + +def test_get_instance_binding_success( + client, managedservice_binding_model_mock, mocker: MockFixture +): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": { - "guid": "test_instance_binding_guid", - "name": "test_instance_binding", - }, - }, + json=managedservice_binding_model_mock, ) # Call the get_instance_binding method @@ -151,23 +178,22 @@ def test_get_instance_binding_success(client, mocker: MockFixture): # noqa: F81 ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_binding_guid" + assert response["metadata"]["guid"] == "mock_instance_binding_guid" + assert response["metadata"]["name"] == "test-instance-binding" + assert response["kind"] == "ManagedServiceBinding" + assert response["spec"]["provider"] == "headscalevpn" -def test_create_instance_binding_success(client, mocker: MockFixture): # noqa: F811 +def test_create_instance_binding_success( + client, managedservice_binding_model_mock, mocker: MockFixture +): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": { - "guid": "test_instance_binding_guid", - "name": "test_instance_binding", - }, - }, + json=managedservice_binding_model_mock, ) # Call the create_instance_binding method @@ -176,11 +202,89 @@ def test_create_instance_binding_success(client, mocker: MockFixture): # noqa: ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_instance_binding_guid" + assert response["metadata"]["guid"] == "mock_instance_binding_guid" + assert response["metadata"]["name"] == "test-instance-binding" + assert response["kind"] == "ManagedServiceBinding" + + +def test_delete_instance_binding_success(client, mocker: MockFixture): + # Mock the httpx.Client.delete method + mock_delete = mocker.patch("httpx.Client.delete") + + # Set up the mock response + mock_delete.return_value = httpx.Response( + status_code=204, + json={"success": True}, + ) + + # Call the delete_instance_binding method + response = client.delete_instance_binding( + name="mock_instance_binding_name", instance_name="mock_instance_name" + ) + + # Validate the response + assert response is None + + +def test_get_instance_binding_success( + client, managedservice_binding_model_mock, mocker: MockFixture +): + mock_get = mocker.patch("httpx.Client.get") + + # Set up the mock response + mock_get.return_value = httpx.Response( + status_code=200, + json=managedservice_binding_model_mock, + ) + + # Call the get_instance_binding method + response = client.get_instance_binding( + name="test-instance-binding", instance_name="mock_instance_name" + ) + + # Validate the response + assert isinstance(response, ManagedServiceBinding) + assert response.metadata.guid == "mock_instance_binding_guid" + assert response.metadata.name == "test-instance-binding" + assert response.kind == "ManagedServiceBinding" + assert response.spec.provider == "headscalevpn" + + +def test_create_instance_binding_success( + client, managedservice_binding_model_mock, mocker: MockFixture +): + # Mock the httpx.Client.post method + mock_post = mocker.patch("httpx.Client.post") + + # Set up the mock response + mock_post.return_value = httpx.Response( + status_code=201, + json=managedservice_binding_model_mock, + ) + + # Call the create_instance_binding method + response = client.create_instance_binding( + body={ + "metadata": { + "name": "test-instance-binding", + "labels": {}, + }, + "spec": { + "instance": "vpn_instance_value", + "provider": "headscalevpn", + }, + }, + instance_name="mock_instance_name", + ) + + # Validate the response + assert isinstance(response, ManagedServiceBinding) + assert response.metadata.guid == "mock_instance_binding_guid" + assert response.metadata.name == "test-instance-binding" + assert response.kind == "ManagedServiceBinding" -def test_delete_instance_binding_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_instance_binding_success(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") @@ -196,4 +300,4 @@ def test_delete_instance_binding_success(client, mocker: MockFixture): # noqa: ) # Validate the response - assert response["success"] is True + assert response is None diff --git a/tests/sync_tests/test_network.py b/tests/sync_tests/test_network.py index 6fe9826..c78b5d7 100644 --- a/tests/sync_tests/test_network.py +++ b/tests/sync_tests/test_network.py @@ -1,34 +1,40 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import network_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import Network, NetworkList +from tests.data.mock_data import network_body, network_model_mock, networklist_model_mock +from tests.utils.fixtures import client -def test_list_networks_success(client, mocker: MockFixture): # noqa: F811 +def test_list_networks_success(client, networklist_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-network", "guid": "mock_network_guid"}], - }, + json=networklist_model_mock, ) # Call the list_networks method response = client.list_networks() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-network", "guid": "mock_network_guid"}] - - -def test_list_networks_not_found(client, mocker: MockFixture): # noqa: F811 + assert isinstance(response, NetworkList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + network = response.items[0] + assert network.metadata.guid == "network-aaaaaaaaaaaaaaaaaaaa" + assert network.metadata.name == "test-network" + assert network.kind == "Network" + assert network.spec.runtime == "cloud" + assert network.status.phase == "InProgress" + assert network.status.status == "Running" + + +def test_list_networks_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -44,27 +50,50 @@ def test_list_networks_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "not found" -def test_create_network_success(client, mocker: MockFixture): # noqa: F811 +def test_get_network_success(client, network_model_mock, mocker: MockFixture): + # Mock the httpx.Client.get method + mock_get = mocker.patch("httpx.Client.get") + + # Set up the mock response + mock_get.return_value = httpx.Response( + status_code=200, + json=network_model_mock, + ) + + # Call the get_network method + response = client.get_network(name="test-network") + + # Validate the response + assert isinstance(response, Network) + assert response.metadata.guid == "network-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "test-network" + assert response.spec.runtime == "cloud" + assert response.status.phase == "InProgress" + assert response.status.status == "Running" + + +def test_create_network_success( + client, network_body, network_model_mock, mocker: MockFixture +): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": {"guid": "mock_network_guid", "name": "test-network"}, - }, + json=network_model_mock, ) # Call the create_network method response = client.create_network(body=network_body) # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["name"] == "test-network" + assert isinstance(response, Network) + assert response.metadata.guid == "network-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "test-network" -def test_create_network_failure(client, mocker: MockFixture): # noqa: F811 +def test_create_network_failure(client, network_body, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") @@ -80,27 +109,7 @@ def test_create_network_failure(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "already exists" -def test_get_network_success(client, mocker: MockFixture): # noqa: F811 - # Mock the httpx.Client.get method - mock_get = mocker.patch("httpx.Client.get") - - # Set up the mock response - mock_get.return_value = httpx.Response( - status_code=200, - json={ - "metadata": {"guid": "mock_network_guid", "name": "test-network"}, - }, - ) - - # Call the get_network method - response = client.get_network(name="test-network") - - # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_network_guid" - - -def test_delete_network_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_network_success(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") @@ -114,4 +123,4 @@ def test_delete_network_success(client, mocker: MockFixture): # noqa: F811 response = client.delete_network(name="test-network") # Validate the response - assert response["success"] is True + assert response is None diff --git a/tests/sync_tests/test_organization.py b/tests/sync_tests/test_organization.py index faf4206..1bb0068 100644 --- a/tests/sync_tests/test_organization.py +++ b/tests/sync_tests/test_organization.py @@ -1,30 +1,38 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import mock_response_organization, organization_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models.organization import Organization +from tests.data.mock_data import mock_response_organization, organization_body +from tests.utils.fixtures import client -def test_get_organization_success(client, mocker: MockFixture): # noqa: F811 +def test_get_organization_success( + client, mock_response_organization, mocker: MockFixture +): mock_get = mocker.patch("httpx.Client.get") + # Use mock_response_organization fixture for GET response mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Organization", - "metadata": {"name": "test-org", "guid": "mock_org_guid"}, - }, + json=mock_response_organization, ) response = client.get_organization() - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test-org", "guid": "mock_org_guid"} + # Validate that response is an Organization model object + assert isinstance(response, Organization) + assert response.metadata.name == "test-org" + assert response.metadata.guid == "mock_org_guid" + assert len(response.spec.users) == 2 + assert response.spec.users[0].emailID == "test.user1@rapyuta-robotics.com" + assert response.spec.users[0].roleInOrganization == "viewer" + assert response.spec.users[1].emailID == "test.user2@rapyuta-robotics.com" + assert response.spec.users[1].roleInOrganization == "admin" -def test_get_organization_unauthorized(client, mocker: MockFixture): # noqa: F811 +def test_get_organization_unauthorized(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") mock_get.return_value = httpx.Response( @@ -39,8 +47,9 @@ def test_get_organization_unauthorized(client, mocker: MockFixture): # noqa: F8 def test_update_organization_success( - client, # noqa: F811 - mock_response_organization, # noqa: F811 + client, + mock_response_organization, + organization_body, mocker: MockFixture, ): mock_put = mocker.patch("httpx.Client.put") @@ -55,5 +64,10 @@ def test_update_organization_success( body=organization_body, ) - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test-org", "guid": "mock_org_guid"} + # Validate that response is an Organization model object + assert isinstance(response, Organization) + assert response.metadata.name == "test-org" + assert response.metadata.guid == "mock_org_guid" + assert len(response.spec.users) == 2 + assert response.spec.users[0].roleInOrganization == "viewer" + assert response.spec.users[1].roleInOrganization == "admin" diff --git a/tests/sync_tests/test_package.py b/tests/sync_tests/test_package.py index c8c8991..c9a5fb8 100644 --- a/tests/sync_tests/test_package.py +++ b/tests/sync_tests/test_package.py @@ -1,34 +1,48 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import package_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import Package, PackageList +from tests.utils.fixtures import client +from tests.data import ( + package_body, + packagelist_model_mock, + cloud_package_model_mock, + device_package_model_mock, +) -def test_list_packages_success(client, mocker: MockFixture): # noqa: F811 +def test_list_packages_success(client, packagelist_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test_package", "guid": "mock_package_guid"}], - }, + json=packagelist_model_mock, ) # Call the list_packages method response = client.list_packages() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test_package", "guid": "mock_package_guid"}] - - -def test_list_packages_not_found(client, mocker: MockFixture): # noqa: F811 + assert isinstance(response, PackageList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 2 + cloud_pkg = response.items[0] + device_pkg = response.items[1] + assert cloud_pkg.metadata.guid == "pkg-aaaaaaaaaaaaaaaaaaaa" + assert cloud_pkg.metadata.name == "gostproxy" + assert cloud_pkg.kind == "Package" + assert cloud_pkg.spec.runtime == "cloud" + assert device_pkg.metadata.guid == "pkg-bbbbbbbbbbbbbbbbbbbb" + assert device_pkg.metadata.name == "database" + assert device_pkg.kind == "Package" + assert device_pkg.spec.runtime == "device" + + +def test_list_packages_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -44,31 +58,51 @@ def test_list_packages_not_found(client, mocker: MockFixture): # noqa: F811 # Validate the exception message assert str(exc.value) == "not found" - # assert response. == "not found" -def test_get_package_success(client, mocker: MockFixture): # noqa: F811 +def test_get_cloud_package_success(client, cloud_package_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_package_guid", "name": "test_package"}, - }, + json=cloud_package_model_mock, ) # Call the get_package method - response = client.get_package(name="mock_package_name") + response = client.get_package(name="gostproxy") # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_package_guid" - assert response.metadata.name == "test_package" + assert isinstance(response, Package) + assert response.metadata.guid == "pkg-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "gostproxy" + assert response.spec.runtime == "cloud" -def test_get_package_not_found(client, mocker: MockFixture): # noqa: F811 +def test_get_device_package_success( + client, device_package_model_mock, mocker: MockFixture +): + # Mock the httpx.Client.get method + mock_get = mocker.patch("httpx.Client.get") + + # Set up the mock response + mock_get.return_value = httpx.Response( + status_code=200, + json=device_package_model_mock, + ) + + # Call the get_package method + response = client.get_package(name="database") + + # Validate the response + assert isinstance(response, Package) + assert response.metadata.guid == "pkg-bbbbbbbbbbbbbbbbbbbb" + assert response.metadata.name == "database" + assert response.spec.runtime == "device" + + +def test_get_package_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -86,22 +120,43 @@ def test_get_package_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "not found" -def test_create_package_success(client, package_body, mocker: MockFixture): # noqa: F811 +def test_create_package_success( + client, package_body, cloud_package_model_mock, mocker: MockFixture +): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": {"guid": "test_package_guid", "name": "test_package"}, - }, + json=cloud_package_model_mock, ) # Call the create_package method response = client.create_package(body=package_body) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_package_guid" - assert response.metadata.name == "test_package" + assert isinstance(response, Package) + assert response.metadata.guid == "pkg-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "gostproxy" + + +def test_delete_package_success(client, mocker: MockFixture): + mock_delete = mocker.patch("httpx.Client.delete") + mock_delete.return_value = httpx.Response( + status_code=204, + json={"success": True}, + ) + response = client.delete_package(name="gostproxy", version="v1.0.0") + assert response is None + + +def test_delete_package_not_found(client, mocker: MockFixture): + mock_delete = mocker.patch("httpx.Client.delete") + mock_delete.return_value = httpx.Response( + status_code=404, + json={"error": "package not found"}, + ) + with pytest.raises(Exception) as exc: + client.delete_package(name="notfound", version="v1.0.0") + assert str(exc.value) == "package not found" diff --git a/tests/sync_tests/test_project.py b/tests/sync_tests/test_project.py index b398052..69c4d13 100644 --- a/tests/sync_tests/test_project.py +++ b/tests/sync_tests/test_project.py @@ -1,38 +1,38 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import ( - mock_response_project, # noqa: F401 - project_body, -) # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import Project, ProjectList +from tests.data import project_body, project_model_mock, projectlist_model_mock +from tests.utils.fixtures import client # Test function for list_projects -def test_list_projects_success(client, mocker: MockFixture): # noqa: F811 +def test_list_projects_success(client, projectlist_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-project", "guid": "mock_project_guid"}], - }, + json=projectlist_model_mock, ) # Call the list_projects method response = client.list_projects() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-project", "guid": "mock_project_guid"}] + assert isinstance(response, ProjectList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + project = response.items[0] + assert project.metadata.guid == "mock_project_guid" + assert project.metadata.name == "test-project" + assert project.kind == "Project" -def test_list_projects_unauthorized(client, mocker: MockFixture): # noqa: F811 +def test_list_projects_unauthorized(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -50,7 +50,7 @@ def test_list_projects_unauthorized(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "unauthorized permission access" -def test_list_projects_not_found(client, mocker: MockFixture): # noqa: F811 +def test_list_projects_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -68,28 +68,26 @@ def test_list_projects_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "not found" -def test_get_project_success(client, mock_response_project, mocker: MockFixture): # noqa: F811 +def test_get_project_success(client, project_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "kind": "Project", - "metadata": {"guid": "test_project_guid", "name": "test_project"}, - }, + json=project_model_mock, ) # Call the get_project method response = client.get_project(project_guid="mock_project_guid") # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "test_project_guid" + assert isinstance(response, Project) + assert response.metadata.guid == "mock_project_guid" + assert response.metadata.name == "test-project" -def test_get_project_not_found(client, mocker: MockFixture): # noqa: F811 +def test_get_project_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -107,25 +105,28 @@ def test_get_project_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "project not found" -def test_create_project_success(client, mock_response_project, mocker: MockFixture): # noqa: F811 +def test_create_project_success( + client, project_body, project_model_mock, mocker: MockFixture +): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json=mock_response_project, + json=project_model_mock, ) # Call the create_project method response = client.create_project(body=project_body) # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_project_guid" + assert isinstance(response, Project) + assert response.metadata.guid == "mock_project_guid" + assert response.metadata.name == "test-project" -def test_create_project_unauthorized(client, mocker: MockFixture): # noqa: F811 +def test_create_project_unauthorized(client, project_body, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") @@ -143,25 +144,28 @@ def test_create_project_unauthorized(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "unauthorized permission access" -def test_update_project_success(client, mock_response_project, mocker: MockFixture): # noqa: F811 +def test_update_project_success( + client, project_body, project_model_mock, mocker: MockFixture +): # Mock the httpx.Client.put method mock_put = mocker.patch("httpx.Client.put") # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, - json=mock_response_project, + json=project_model_mock, ) # Call the update_project method response = client.update_project(project_guid="mock_project_guid", body=project_body) # Validate the response - assert isinstance(response, Munch) - assert response["metadata"]["guid"] == "mock_project_guid" + assert isinstance(response, Project) + assert response.metadata.guid == "mock_project_guid" + assert response.metadata.name == "test-project" -def test_delete_project_success(client, mock_response_project, mocker: MockFixture): # noqa: F811 +def test_delete_project_success(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") @@ -172,4 +176,4 @@ def test_delete_project_success(client, mock_response_project, mocker: MockFixtu response = client.delete_project(project_guid="mock_project_guid") # Validate the response - assert response["success"] is True + assert response is None diff --git a/tests/sync_tests/test_secret.py b/tests/sync_tests/test_secret.py index 5d1e6ad..055c2ad 100644 --- a/tests/sync_tests/test_secret.py +++ b/tests/sync_tests/test_secret.py @@ -1,34 +1,37 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import secret_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import Secret, SecretList +from tests.data.mock_data import secret_body, secret_model_mock, secretlist_model_mock +from tests.utils.fixtures import client -def test_list_secrets_success(client, mocker: MockFixture): # noqa: F811 +def test_list_secrets_success(client, secretlist_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-secret", "guid": "mock_secret_guid"}], - }, + json=secretlist_model_mock, ) # Call the list_secrets method response = client.list_secrets() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [{"name": "test-secret", "guid": "mock_secret_guid"}] + assert isinstance(response, SecretList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + secret = response.items[0] + assert secret.metadata.guid == "secret-aaaaaaaaaaaaaaaaaaaa" + assert secret.metadata.name == "test_secret" + assert secret.kind == "Secret" -def test_list_secrets_not_found(client, mocker: MockFixture): # noqa: F811 +def test_list_secrets_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -44,27 +47,28 @@ def test_list_secrets_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "not found" -def test_create_secret_success(client, mocker: MockFixture): # noqa: F811 +def test_create_secret_success( + client, secret_body, secret_model_mock, mocker: MockFixture +): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": {"guid": "test_secret_guid", "name": "test_secret"}, - }, + json=secret_model_mock, ) # Call the create_secret method response = client.create_secret(secret_body) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_secret_guid" + assert isinstance(response, Secret) + assert response.metadata.guid == "secret-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "test_secret" -def test_create_secret_already_exists(client, mocker: MockFixture): # noqa: F811 +def test_create_secret_already_exists(client, secret_body, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") @@ -80,27 +84,28 @@ def test_create_secret_already_exists(client, mocker: MockFixture): # noqa: F81 assert str(exc.value) == "secret already exists" -def test_update_secret_success(client, mocker: MockFixture): # noqa: F811 +def test_update_secret_success( + client, secret_body, secret_model_mock, mocker: MockFixture +): # Mock the httpx.Client.put method mock_put = mocker.patch("httpx.Client.put") # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_secret_guid", "name": "test_secret"}, - }, + json=secret_model_mock, ) # Call the update_secret method - response = client.update_secret("mock_secret_guid", body=secret_body) + response = client.update_secret("secret-aaaaaaaaaaaaaaaaaaaa", body=secret_body) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_secret_guid" + assert isinstance(response, Secret) + assert response.metadata.guid == "secret-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "test_secret" -def test_delete_secret_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_secret_success(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") @@ -111,28 +116,26 @@ def test_delete_secret_success(client, mocker: MockFixture): # noqa: F811 ) # Call the delete_secret method - response = client.delete_secret("mock_secret_guid") + response = client.delete_secret("secret-aaaaaaaaaaaaaaaaaaaa") # Validate the response - assert response == {"success": True} + assert response is None -def test_get_secret_success(client, mocker: MockFixture): # noqa: F811 +def test_get_secret_success(client, secret_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_secret_guid", "name": "test_secret"}, - }, + json=secret_model_mock, ) # Call the get_secret method - response = client.get_secret("mock_secret_guid") + response = client.get_secret("secret-aaaaaaaaaaaaaaaaaaaa") # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_secret_guid" + assert isinstance(response, Secret) + assert response.metadata.guid == "secret-aaaaaaaaaaaaaaaaaaaa" assert response.metadata.name == "test_secret" diff --git a/tests/sync_tests/test_staticroute.py b/tests/sync_tests/test_staticroute.py index 24bb1eb..22917dc 100644 --- a/tests/sync_tests/test_staticroute.py +++ b/tests/sync_tests/test_staticroute.py @@ -1,36 +1,43 @@ import httpx import pytest -from munch import Munch from pytest_mock import MockFixture -from tests.data.mock_data import staticroute_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from rapyuta_io_sdk_v2.models import StaticRoute, StaticRouteList +from tests.utils.fixtures import client +from tests.data.mock_data import ( + staticroutelist_model_mock as staticroutelist_model_mock, + staticroute_model_mock as staticroute_model_mock, + staticroute_body as staticroute_body, +) -def test_list_staticroutes_success(client, mocker: MockFixture): # noqa: F811 +def test_list_staticroutes_success( + client, staticroutelist_model_mock, mocker: MockFixture +): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock responses for pagination mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"continue": 1}, - "items": [{"name": "test-staticroute", "guid": "mock_staticroute_guid"}], - }, + json=staticroutelist_model_mock, ) # Call the list_staticroutes method response = client.list_staticroutes() # Validate the response - assert isinstance(response, Munch) - assert response["items"] == [ - {"name": "test-staticroute", "guid": "mock_staticroute_guid"} - ] + assert isinstance(response, StaticRouteList) + assert response.metadata.continue_ == 1 + assert len(response.items) == 1 + staticroute = response.items[0] + assert staticroute.metadata.guid == "staticroute-aaaaaaaaaaaaaaaaaaaa" + assert staticroute.metadata.name == "test-staticroute" + assert staticroute.kind == "StaticRoute" -def test_list_staticroutes_not_found(client, mocker: MockFixture): # noqa: F811 +def test_list_staticroutes_not_found(client, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") @@ -46,27 +53,28 @@ def test_list_staticroutes_not_found(client, mocker: MockFixture): # noqa: F811 assert str(exc.value) == "not found" -def test_create_staticroute_success(client, mocker: MockFixture): # noqa: F811 +def test_create_staticroute_success( + client, staticroute_body, staticroute_model_mock, mocker: MockFixture +): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") # Set up the mock response mock_post.return_value = httpx.Response( status_code=201, - json={ - "metadata": {"guid": "test_staticroute_guid", "name": "test_staticroute"}, - }, + json=staticroute_model_mock, ) # Call the create_staticroute method response = client.create_staticroute(body=staticroute_body) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_staticroute_guid" + assert isinstance(response, StaticRoute) + assert response.metadata.guid == "staticroute-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "test-staticroute" -def test_create_staticroute_bad_request(client, mocker: MockFixture): # noqa: F811 +def test_create_staticroute_bad_request(client, staticroute_body, mocker: MockFixture): # Mock the httpx.Client.post method mock_post = mocker.patch("httpx.Client.post") @@ -82,36 +90,35 @@ def test_create_staticroute_bad_request(client, mocker: MockFixture): # noqa: F assert str(exc.value) == "already exists" -def test_get_staticroute_success(client, mocker: MockFixture): # noqa: F811 +def test_get_staticroute_success(client, staticroute_model_mock, mocker: MockFixture): # Mock the httpx.Client.get method mock_get = mocker.patch("httpx.Client.get") # Set up the mock response mock_get.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_staticroute_guid", "name": "test_staticroute"}, - }, + json=staticroute_model_mock, ) # Call the get_staticroute method response = client.get_staticroute(name="mock_staticroute_name") # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_staticroute_guid" + assert isinstance(response, StaticRoute) + assert response.metadata.guid == "staticroute-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "test-staticroute" -def test_update_staticroute_success(client, mocker: MockFixture): # noqa: F811 +def test_update_staticroute_success( + client, staticroute_body, staticroute_model_mock, mocker: MockFixture +): # Mock the httpx.Client.put method mock_put = mocker.patch("httpx.Client.put") # Set up the mock response mock_put.return_value = httpx.Response( status_code=200, - json={ - "metadata": {"guid": "test_staticroute_guid", "name": "test_staticroute"}, - }, + json=staticroute_model_mock, ) # Call the update_staticroute method @@ -120,11 +127,12 @@ def test_update_staticroute_success(client, mocker: MockFixture): # noqa: F811 ) # Validate the response - assert isinstance(response, Munch) - assert response.metadata.guid == "test_staticroute_guid" + assert isinstance(response, StaticRoute) + assert response.metadata.guid == "staticroute-aaaaaaaaaaaaaaaaaaaa" + assert response.metadata.name == "test-staticroute" -def test_delete_staticroute_success(client, mocker: MockFixture): # noqa: F811 +def test_delete_staticroute_success(client, mocker: MockFixture): # Mock the httpx.Client.delete method mock_delete = mocker.patch("httpx.Client.delete") @@ -138,4 +146,4 @@ def test_delete_staticroute_success(client, mocker: MockFixture): # noqa: F811 response = client.delete_staticroute(name="mock_staticroute_name") # Validate the response - assert response["success"] is True + assert response is None diff --git a/tests/sync_tests/test_user.py b/tests/sync_tests/test_user.py index 7316295..7947519 100644 --- a/tests/sync_tests/test_user.py +++ b/tests/sync_tests/test_user.py @@ -1,30 +1,38 @@ import httpx import pytest -from munch import Munch -from pytest_mock import MockFixture -from tests.data.mock_data import mock_response_user, user_body # noqa: F401 -from tests.utils.fixtures import client # noqa: F401 +# ruff: noqa: F811, F401 +from pytest_mock import MockFixture +from rapyuta_io_sdk_v2.exceptions import UnauthorizedAccessError +from tests.data.mock_data import mock_response_user, user_body +from tests.utils.fixtures import client -def test_get_user_success(client, mocker: MockFixture): # noqa: F811 +def test_get_user_success(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") mock_get.return_value = httpx.Response( status_code=200, json={ "kind": "User", - "metadata": {"name": "test-org", "guid": "mock_org_guid"}, + "metadata": {"name": "test user", "guid": "mock_user_guid"}, + "spec": { + "emailID": "test.user@example.com", + "firstName": "Test", + "lastName": "User", + "userGUID": "mock_user_guid", + "role": "admin", + }, }, ) response = client.get_user() - - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test-org", "guid": "mock_org_guid"} + assert response.metadata.name == "test user" + assert response.metadata.guid == "mock_user_guid" + assert response.spec.emailID == "test.user@example.com" -def test_get_user_unauthorized(client, mocker: MockFixture): # noqa: F811 +def test_get_user_unauthorized(client, mocker: MockFixture): mock_get = mocker.patch("httpx.Client.get") mock_get.return_value = httpx.Response( @@ -32,33 +40,24 @@ def test_get_user_unauthorized(client, mocker: MockFixture): # noqa: F811 json={"error": "user cannot be authenticated"}, ) - with pytest.raises(Exception) as exc: + with pytest.raises(UnauthorizedAccessError) as exc: client.get_user() + assert "user cannot be authenticated" in str(exc.value) - assert str(exc.value) == "user cannot be authenticated" - -def test_update_user_success( - client, # noqa: F811 - mock_response_user, # noqa: F811 - mocker: MockFixture, -): +def test_update_user_success(client, user_body, mock_response_user, mocker: MockFixture): mock_put = mocker.patch("httpx.Client.put") - mock_put.return_value = httpx.Response( status_code=200, json=mock_response_user, ) + response = client.update_user(body=user_body) + assert response.metadata.name == "test user" + assert response.metadata.guid == "mock_user_guid" + assert response.spec.emailID == "test.user@example.com" - response = client.update_user( - body=user_body, - ) - - assert isinstance(response, Munch) - assert response["metadata"] == {"name": "test user", "guid": "mock_user_guid"} - -def test_update_user_unauthorized(client, mocker: MockFixture): # noqa: F811 +def test_update_user_unauthorized(client, user_body, mocker: MockFixture): mock_put = mocker.patch("httpx.Client.put") mock_put.return_value = httpx.Response( @@ -66,7 +65,6 @@ def test_update_user_unauthorized(client, mocker: MockFixture): # noqa: F811 json={"error": "user cannot be authenticated"}, ) - with pytest.raises(Exception) as exc: + with pytest.raises(UnauthorizedAccessError) as exc: client.update_user(user_body) - - assert str(exc.value) == "user cannot be authenticated" + assert "user cannot be authenticated" in str(exc.value) diff --git a/uv.lock b/uv.lock index f9bfd1e..c0459ac 100644 --- a/uv.lock +++ b/uv.lock @@ -1,31 +1,29 @@ version = 1 -requires-python = ">=3.8" +revision = 2 +requires-python = ">=3.10" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "anyio" -version = "4.5.2" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 }, + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] [[package]] @@ -35,106 +33,224 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mock" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/58/fa6b3147951a8d82cc78e628dffee0aa5838328c52ebfee4e0ddceb5d92b/asyncmock-0.4.2.tar.gz", hash = "sha256:c251889d542e98fe5f7ece2b5b8643b7d62b50a5657d34a4cbce8a1d5170d750", size = 3191 } +sdist = { url = "https://files.pythonhosted.org/packages/c8/58/fa6b3147951a8d82cc78e628dffee0aa5838328c52ebfee4e0ddceb5d92b/asyncmock-0.4.2.tar.gz", hash = "sha256:c251889d542e98fe5f7ece2b5b8643b7d62b50a5657d34a4cbce8a1d5170d750", size = 3191, upload-time = "2020-03-15T21:09:12.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e3/873f433eca053c92d3cdb9336a379ee025bc1a86d4624ef87bf97a9ac7bc/asyncmock-0.4.2-py3-none-any.whl", hash = "sha256:fd8bc4e7813251a8959d1140924ccba3adbbc7af885dba7047c67f73c0b664b1", size = 4190 }, + { url = "https://files.pythonhosted.org/packages/03/e3/873f433eca053c92d3cdb9336a379ee025bc1a86d4624ef87bf97a9ac7bc/asyncmock-0.4.2-py3-none-any.whl", hash = "sha256:fd8bc4e7813251a8959d1140924ccba3adbbc7af885dba7047c67f73c0b664b1", size = 4190, upload-time = "2020-03-15T21:09:09.066Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" -version = "7.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, - { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, - { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, - { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, - { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, - { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, - { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, - { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, - { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, - { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, - { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, - { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, - { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, - { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, - { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, - { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, - { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, - { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, - { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, - { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, - { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, - { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, - { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, - { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, - { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, - { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, - { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, - { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, - { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, - { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, - { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, - { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, - { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, - { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, - { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, - { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, - { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, - { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, - { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, - { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, - { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, - { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, - { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, - { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, - { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, - { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, - { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, - { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, - { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, - { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, - { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, - { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, - { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, - { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, - { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, - { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, - { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, - { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, - { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, - { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, - { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, - { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, - { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, - { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, - { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, - { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, - { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, - { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, - { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, - { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, - { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +version = "7.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/9698d57a3b11704c7b89b21d69e9d23ecf80d538cabb536c8b63f4a12322/coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b", size = 815210, upload-time = "2025-11-10T00:13:17.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/68/b53157115ef76d50d1d916d6240e5cd5b3c14dba8ba1b984632b8221fc2e/coverage-7.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5", size = 216377, upload-time = "2025-11-10T00:10:27.317Z" }, + { url = "https://files.pythonhosted.org/packages/14/c1/d2f9d8e37123fe6e7ab8afcaab8195f13bc84a8b2f449a533fd4812ac724/coverage-7.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7", size = 216892, upload-time = "2025-11-10T00:10:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/83/73/18f05d8010149b650ed97ee5c9f7e4ae68c05c7d913391523281e41c2495/coverage-7.11.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb", size = 243650, upload-time = "2025-11-10T00:10:32.392Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/c0cbb296c0ecc6dcbd70f4b473fcd7fe4517bbef8b09f4326d78f38adb87/coverage-7.11.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1", size = 245478, upload-time = "2025-11-10T00:10:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9a/dad288cf9faa142a14e75e39dc646d968b93d74e15c83e9b13fd628f2cb3/coverage-7.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c", size = 247337, upload-time = "2025-11-10T00:10:35.655Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ba/f6148ebf5547b3502013175e41bf3107a4e34b7dd19f9793a6ce0e1cd61f/coverage-7.11.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31", size = 244328, upload-time = "2025-11-10T00:10:37.459Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4d/b93784d0b593c5df89a0d48cbbd2d0963e0ca089eaf877405849792e46d3/coverage-7.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2", size = 245381, upload-time = "2025-11-10T00:10:39.229Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/6735bfd4f0f736d457642ee056a570d704c9d57fdcd5c91ea5d6b15c944e/coverage-7.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507", size = 243390, upload-time = "2025-11-10T00:10:40.984Z" }, + { url = "https://files.pythonhosted.org/packages/db/3d/7ba68ed52d1873d450aefd8d2f5a353e67b421915cb6c174e4222c7b918c/coverage-7.11.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832", size = 243654, upload-time = "2025-11-10T00:10:42.496Z" }, + { url = "https://files.pythonhosted.org/packages/14/26/be2720c4c7bf73c6591ae4ab503a7b5a31c7a60ced6dba855cfcb4a5af7e/coverage-7.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e", size = 244272, upload-time = "2025-11-10T00:10:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/90/20/086f5697780df146dbc0df4ae9b6db2b23ddf5aa550f977b2825137728e9/coverage-7.11.3-cp310-cp310-win32.whl", hash = "sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb", size = 218969, upload-time = "2025-11-10T00:10:45.863Z" }, + { url = "https://files.pythonhosted.org/packages/98/5c/cc6faba945ede5088156da7770e30d06c38b8591785ac99bcfb2074f9ef6/coverage-7.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8", size = 219903, upload-time = "2025-11-10T00:10:47.676Z" }, + { url = "https://files.pythonhosted.org/packages/92/92/43a961c0f57b666d01c92bcd960c7f93677de5e4ee7ca722564ad6dee0fa/coverage-7.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1", size = 216504, upload-time = "2025-11-10T00:10:49.524Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5c/dbfc73329726aef26dbf7fefef81b8a2afd1789343a579ea6d99bf15d26e/coverage-7.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06", size = 217006, upload-time = "2025-11-10T00:10:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e0/878c84fb6661964bc435beb1e28c050650aa30e4c1cdc12341e298700bda/coverage-7.11.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80", size = 247415, upload-time = "2025-11-10T00:10:52.805Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/0677e78b1e6a13527f39c4b39c767b351e256b333050539861c63f98bd61/coverage-7.11.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa", size = 249332, upload-time = "2025-11-10T00:10:54.35Z" }, + { url = "https://files.pythonhosted.org/packages/54/90/25fc343e4ce35514262451456de0953bcae5b37dda248aed50ee51234cee/coverage-7.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297", size = 251443, upload-time = "2025-11-10T00:10:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/13/56/bc02bbc890fd8b155a64285c93e2ab38647486701ac9c980d457cdae857a/coverage-7.11.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362", size = 247554, upload-time = "2025-11-10T00:10:57.829Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ab/0318888d091d799a82d788c1e8d8bd280f1d5c41662bbb6e11187efe33e8/coverage-7.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87", size = 249139, upload-time = "2025-11-10T00:10:59.465Z" }, + { url = "https://files.pythonhosted.org/packages/79/d8/3ee50929c4cd36fcfcc0f45d753337001001116c8a5b8dd18d27ea645737/coverage-7.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200", size = 247209, upload-time = "2025-11-10T00:11:01.432Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/3cf06e327401c293e60c962b4b8a2ceb7167c1a428a02be3adbd1d7c7e4c/coverage-7.11.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4", size = 246936, upload-time = "2025-11-10T00:11:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/99/0b/ffc03dc8f4083817900fd367110015ef4dd227b37284104a5eb5edc9c106/coverage-7.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060", size = 247835, upload-time = "2025-11-10T00:11:04.405Z" }, + { url = "https://files.pythonhosted.org/packages/17/4d/dbe54609ee066553d0bcdcdf108b177c78dab836292bee43f96d6a5674d1/coverage-7.11.3-cp311-cp311-win32.whl", hash = "sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7", size = 218994, upload-time = "2025-11-10T00:11:05.966Z" }, + { url = "https://files.pythonhosted.org/packages/94/11/8e7155df53f99553ad8114054806c01a2c0b08f303ea7e38b9831652d83d/coverage-7.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55", size = 219926, upload-time = "2025-11-10T00:11:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/bea91b6a9e35d89c89a1cd5824bc72e45151a9c2a9ca0b50d9e9a85e3ae3/coverage-7.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc", size = 218599, upload-time = "2025-11-10T00:11:09.578Z" }, + { url = "https://files.pythonhosted.org/packages/c2/39/af056ec7a27c487e25c7f6b6e51d2ee9821dba1863173ddf4dc2eebef4f7/coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f", size = 216676, upload-time = "2025-11-10T00:11:11.566Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f8/21126d34b174d037b5d01bea39077725cbb9a0da94a95c5f96929c695433/coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e", size = 217034, upload-time = "2025-11-10T00:11:13.12Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3f/0fd35f35658cdd11f7686303214bd5908225838f374db47f9e457c8d6df8/coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a", size = 248531, upload-time = "2025-11-10T00:11:15.023Z" }, + { url = "https://files.pythonhosted.org/packages/8f/59/0bfc5900fc15ce4fd186e092451de776bef244565c840c9c026fd50857e1/coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1", size = 251290, upload-time = "2025-11-10T00:11:16.628Z" }, + { url = "https://files.pythonhosted.org/packages/71/88/d5c184001fa2ac82edf1b8f2cd91894d2230d7c309e937c54c796176e35b/coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd", size = 252375, upload-time = "2025-11-10T00:11:18.249Z" }, + { url = "https://files.pythonhosted.org/packages/5c/29/f60af9f823bf62c7a00ce1ac88441b9a9a467e499493e5cc65028c8b8dd2/coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5", size = 248946, upload-time = "2025-11-10T00:11:20.202Z" }, + { url = "https://files.pythonhosted.org/packages/67/16/4662790f3b1e03fce5280cad93fd18711c35980beb3c6f28dca41b5230c6/coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e", size = 250310, upload-time = "2025-11-10T00:11:21.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dd6c2e28308a83e5fc1ee602f8204bd3aa5af685c104cb54499230cf56db/coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044", size = 248461, upload-time = "2025-11-10T00:11:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/16/fe/b71af12be9f59dc9eb060688fa19a95bf3223f56c5af1e9861dfa2275d2c/coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7", size = 248039, upload-time = "2025-11-10T00:11:25.07Z" }, + { url = "https://files.pythonhosted.org/packages/11/b8/023b2003a2cd96bdf607afe03d9b96c763cab6d76e024abe4473707c4eb8/coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405", size = 249903, upload-time = "2025-11-10T00:11:26.992Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/5f1076311aa67b1fa4687a724cc044346380e90ce7d94fec09fd384aa5fd/coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e", size = 219201, upload-time = "2025-11-10T00:11:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/4f/24/d21688f48fe9fcc778956680fd5aaf69f4e23b245b7c7a4755cbd421d25b/coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055", size = 220012, upload-time = "2025-11-10T00:11:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9e/d5eb508065f291456378aa9b16698b8417d87cb084c2b597f3beb00a8084/coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f", size = 218652, upload-time = "2025-11-10T00:11:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/d8572c058211c7d976f24dab71999a565501fb5b3cdcb59cf782f19c4acb/coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36", size = 216694, upload-time = "2025-11-10T00:11:34.296Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f6/b6f9764d90c0ce1bce8d995649fa307fff21f4727b8d950fa2843b7b0de5/coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e", size = 217065, upload-time = "2025-11-10T00:11:36.281Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/a12cb424063019fd077b5be474258a0ed8369b92b6d0058e673f0a945982/coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2", size = 248062, upload-time = "2025-11-10T00:11:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/dab1a4e8e75ce053d14259d3d7485d68528a662e286e184685ea49e71156/coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63", size = 250657, upload-time = "2025-11-10T00:11:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/3f/89/a14f256438324f33bae36f9a1a7137729bf26b0a43f5eda60b147ec7c8c7/coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3", size = 251900, upload-time = "2025-11-10T00:11:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/04/07/75b0d476eb349f1296486b1418b44f2d8780cc8db47493de3755e5340076/coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5", size = 248254, upload-time = "2025-11-10T00:11:43.27Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4b/0c486581fa72873489ca092c52792d008a17954aa352809a7cbe6cf0bf07/coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5", size = 250041, upload-time = "2025-11-10T00:11:45.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/a3/0059dafb240ae3e3291f81b8de00e9c511d3dd41d687a227dd4b529be591/coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7", size = 248004, upload-time = "2025-11-10T00:11:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/83/93/967d9662b1eb8c7c46917dcc7e4c1875724ac3e73c3cb78e86d7a0ac719d/coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5", size = 247828, upload-time = "2025-11-10T00:11:48.563Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1c/5077493c03215701e212767e470b794548d817dfc6247a4718832cc71fac/coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094", size = 249588, upload-time = "2025-11-10T00:11:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a5/77f64de461016e7da3e05d7d07975c89756fe672753e4cf74417fc9b9052/coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c", size = 219223, upload-time = "2025-11-10T00:11:52.184Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/ec51a3c1a59d225b44bdd3a4d463135b3159a535c2686fac965b698524f4/coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2", size = 220033, upload-time = "2025-11-10T00:11:53.871Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/e0ce39746ed558564c16f2cc25fa95ce6fc9fa8bfb3b9e62855d4386b886/coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944", size = 218661, upload-time = "2025-11-10T00:11:55.597Z" }, + { url = "https://files.pythonhosted.org/packages/46/cb/483f130bc56cbbad2638248915d97b185374d58b19e3cc3107359715949f/coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428", size = 217389, upload-time = "2025-11-10T00:11:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ae/81f89bae3afef75553cf10e62feb57551535d16fd5859b9ee5a2a97ddd27/coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a", size = 217742, upload-time = "2025-11-10T00:11:59.519Z" }, + { url = "https://files.pythonhosted.org/packages/db/6e/a0fb897041949888191a49c36afd5c6f5d9f5fd757e0b0cd99ec198a324b/coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655", size = 259049, upload-time = "2025-11-10T00:12:01.592Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/d13acc67eb402d91eb94b9bd60593411799aed09ce176ee8d8c0e39c94ca/coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7", size = 261113, upload-time = "2025-11-10T00:12:03.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/07/a6868893c48191d60406df4356aa7f0f74e6de34ef1f03af0d49183e0fa1/coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d", size = 263546, upload-time = "2025-11-10T00:12:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/e5/28598f70b2c1098332bac47925806353b3313511d984841111e6e760c016/coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f", size = 258260, upload-time = "2025-11-10T00:12:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/0e/58/58e2d9e6455a4ed746a480c4b9cf96dc3cb2a6b8f3efbee5efd33ae24b06/coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0", size = 261121, upload-time = "2025-11-10T00:12:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/38803eefb9b0409934cbc5a14e3978f0c85cb251d2b6f6a369067a7105a0/coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739", size = 258736, upload-time = "2025-11-10T00:12:11.195Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/f94683167156e93677b3442be1d4ca70cb33718df32a2eea44a5898f04f6/coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71", size = 257625, upload-time = "2025-11-10T00:12:12.843Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/42d0bf1bc6bfa7d65f52299a31daaa866b4c11000855d753857fe78260ac/coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76", size = 259827, upload-time = "2025-11-10T00:12:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/d3/76/5682719f5d5fbedb0c624c9851ef847407cae23362deb941f185f489c54e/coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c", size = 219897, upload-time = "2025-11-10T00:12:17.274Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/1da511d0ac3d39e6676fa6cc5ec35320bbf1cebb9b24e9ee7548ee4e931a/coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac", size = 220959, upload-time = "2025-11-10T00:12:19.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9d/e255da6a04e9ec5f7b633c54c0fdfa221a9e03550b67a9c83217de12e96c/coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc", size = 219234, upload-time = "2025-11-10T00:12:21.251Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/634ec396e45aded1772dccf6c236e3e7c9604bc47b816e928f32ce7987d1/coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c", size = 216746, upload-time = "2025-11-10T00:12:23.089Z" }, + { url = "https://files.pythonhosted.org/packages/28/76/1079547f9d46f9c7c7d0dad35b6873c98bc5aa721eeabceafabd722cd5e7/coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203", size = 217077, upload-time = "2025-11-10T00:12:24.863Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/6ad80d6ae0d7cb743b9a98df8bb88b1ff3dc54491508a4a97549c2b83400/coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240", size = 248122, upload-time = "2025-11-10T00:12:26.553Z" }, + { url = "https://files.pythonhosted.org/packages/20/1d/784b87270784b0b88e4beec9d028e8d58f73ae248032579c63ad2ac6f69a/coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83", size = 250638, upload-time = "2025-11-10T00:12:28.555Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/b6dd31e23e004e9de84d1a8672cd3d73e50f5dae65dbd0f03fa2cdde6100/coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902", size = 251972, upload-time = "2025-11-10T00:12:30.246Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ef/f9c64d76faac56b82daa036b34d4fe9ab55eb37f22062e68e9470583e688/coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428", size = 248147, upload-time = "2025-11-10T00:12:32.195Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/5b666f90a8f8053bd264a1ce693d2edef2368e518afe70680070fca13ecd/coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75", size = 249995, upload-time = "2025-11-10T00:12:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/eb/7b/871e991ffb5d067f8e67ffb635dabba65b231d6e0eb724a4a558f4a702a5/coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704", size = 247948, upload-time = "2025-11-10T00:12:36.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/ce454f0af9609431b06dbe5485fc9d1c35ddc387e32ae8e374f49005748b/coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b", size = 247770, upload-time = "2025-11-10T00:12:38.167Z" }, + { url = "https://files.pythonhosted.org/packages/61/8f/79002cb58a61dfbd2085de7d0a46311ef2476823e7938db80284cedd2428/coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131", size = 249431, upload-time = "2025-11-10T00:12:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/d06685dae97468ed22999440f2f2f5060940ab0e7952a7295f236d98cce7/coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a", size = 219508, upload-time = "2025-11-10T00:12:42.231Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/770cd07706a3598c545f62d75adf2e5bd3791bffccdcf708ec383ad42559/coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86", size = 220325, upload-time = "2025-11-10T00:12:44.065Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ac/6a1c507899b6fb1b9a56069954365f655956bcc648e150ce64c2b0ecbed8/coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e", size = 218899, upload-time = "2025-11-10T00:12:46.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/58/142cd838d960cd740654d094f7b0300d7b81534bb7304437d2439fb685fb/coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df", size = 217471, upload-time = "2025-11-10T00:12:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2c/2f44d39eb33e41ab3aba80571daad32e0f67076afcf27cb443f9e5b5a3ee/coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001", size = 217742, upload-time = "2025-11-10T00:12:50.182Z" }, + { url = "https://files.pythonhosted.org/packages/32/76/8ebc66c3c699f4de3174a43424c34c086323cd93c4930ab0f835731c443a/coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de", size = 259120, upload-time = "2025-11-10T00:12:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/78a3302b9595f331b86e4f12dfbd9252c8e93d97b8631500888f9a3a2af7/coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926", size = 261229, upload-time = "2025-11-10T00:12:54.667Z" }, + { url = "https://files.pythonhosted.org/packages/07/59/1a9c0844dadef2a6efac07316d9781e6c5a3f3ea7e5e701411e99d619bfd/coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd", size = 263642, upload-time = "2025-11-10T00:12:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/37/86/66c15d190a8e82eee777793cabde730640f555db3c020a179625a2ad5320/coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac", size = 258193, upload-time = "2025-11-10T00:12:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c7/4a4aeb25cb6f83c3ec4763e5f7cc78da1c6d4ef9e22128562204b7f39390/coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46", size = 261107, upload-time = "2025-11-10T00:13:00.502Z" }, + { url = "https://files.pythonhosted.org/packages/ed/91/b986b5035f23cf0272446298967ecdd2c3c0105ee31f66f7e6b6948fd7f8/coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64", size = 258717, upload-time = "2025-11-10T00:13:02.747Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/6c084997f5a04d050c513545d3344bfa17bd3b67f143f388b5757d762b0b/coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f", size = 257541, upload-time = "2025-11-10T00:13:04.689Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c5/38e642917e406930cb67941210a366ccffa767365c8f8d9ec0f465a8b218/coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820", size = 259872, upload-time = "2025-11-10T00:13:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/b7/67/5e812979d20c167f81dbf9374048e0193ebe64c59a3d93d7d947b07865fa/coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237", size = 220289, upload-time = "2025-11-10T00:13:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/24/3a/b72573802672b680703e0df071faadfab7dcd4d659aaaffc4626bc8bbde8/coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9", size = 221398, upload-time = "2025-11-10T00:13:10.734Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4e/649628f28d38bad81e4e8eb3f78759d20ac173e3c456ac629123815feb40/coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd", size = 219435, upload-time = "2025-11-10T00:13:12.712Z" }, + { url = "https://files.pythonhosted.org/packages/19/8f/92bdd27b067204b99f396a1414d6342122f3e2663459baf787108a6b8b84/coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe", size = 208478, upload-time = "2025-11-10T00:13:14.908Z" }, ] [package.optional-dependencies] @@ -144,245 +260,257 @@ toml = [ [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" -version = "1.0.6" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, - { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "mock" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/ab/41d09a46985ead5839d8be987acda54b5bb93f713b3969cc0be4f81c455b/mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d", size = 80232 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", size = 30938 }, -] - -[[package]] -name = "munch" -version = "4.0.0" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/2b/45098135b5f9f13221820d90f9e0516e11a2a0f55012c13b081d202b782a/munch-4.0.0.tar.gz", hash = "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235", size = 19089 } +sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796, upload-time = "2025-03-03T12:31:42.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/b3/7c69b37f03260a061883bec0e7b05be7117c1b1c85f5212c72c8c2bc3c8c/munch-4.0.0-py2.py3-none-any.whl", hash = "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4", size = 9950 }, + { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload-time = "2025-03-03T12:31:41.518Z" }, ] [[package]] name = "packaging" -version = "24.1" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pydantic" -version = "2.10.5" +version = "2.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, - { url = "https://files.pythonhosted.org/packages/43/53/13e9917fc69c0a4aea06fd63ed6a8d6cda9cf140ca9584d49c1650b0ef5e/pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506", size = 1899595 }, - { url = "https://files.pythonhosted.org/packages/f4/20/26c549249769ed84877f862f7bb93f89a6ee08b4bee1ed8781616b7fbb5e/pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320", size = 1775010 }, - { url = "https://files.pythonhosted.org/packages/35/eb/8234e05452d92d2b102ffa1b56d801c3567e628fdc63f02080fdfc68fd5e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145", size = 1830727 }, - { url = "https://files.pythonhosted.org/packages/8f/df/59f915c8b929d5f61e5a46accf748a87110ba145156f9326d1a7d28912b2/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1", size = 1868393 }, - { url = "https://files.pythonhosted.org/packages/d5/52/81cf4071dca654d485c277c581db368b0c95b2b883f4d7b736ab54f72ddf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228", size = 2040300 }, - { url = "https://files.pythonhosted.org/packages/9c/00/05197ce1614f5c08d7a06e1d39d5d8e704dc81971b2719af134b844e2eaf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046", size = 2738785 }, - { url = "https://files.pythonhosted.org/packages/f7/a3/5f19bc495793546825ab160e530330c2afcee2281c02b5ffafd0b32ac05e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5", size = 1996493 }, - { url = "https://files.pythonhosted.org/packages/ed/e8/e0102c2ec153dc3eed88aea03990e1b06cfbca532916b8a48173245afe60/pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a", size = 1998544 }, - { url = "https://files.pythonhosted.org/packages/fb/a3/4be70845b555bd80aaee9f9812a7cf3df81550bce6dadb3cfee9c5d8421d/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d", size = 2007449 }, - { url = "https://files.pythonhosted.org/packages/e3/9f/b779ed2480ba355c054e6d7ea77792467631d674b13d8257085a4bc7dcda/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9", size = 2129460 }, - { url = "https://files.pythonhosted.org/packages/a0/f0/a6ab0681f6e95260c7fbf552874af7302f2ea37b459f9b7f00698f875492/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da", size = 2159609 }, - { url = "https://files.pythonhosted.org/packages/8a/2b/e1059506795104349712fbca647b18b3f4a7fd541c099e6259717441e1e0/pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b", size = 1819886 }, - { url = "https://files.pythonhosted.org/packages/aa/6d/df49c17f024dfc58db0bacc7b03610058018dd2ea2eaf748ccbada4c3d06/pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad", size = 1980773 }, - { url = "https://files.pythonhosted.org/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475 }, - { url = "https://files.pythonhosted.org/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112 }, - { url = "https://files.pythonhosted.org/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780 }, - { url = "https://files.pythonhosted.org/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943 }, - { url = "https://files.pythonhosted.org/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492 }, - { url = "https://files.pythonhosted.org/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714 }, - { url = "https://files.pythonhosted.org/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163 }, - { url = "https://files.pythonhosted.org/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217 }, - { url = "https://files.pythonhosted.org/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899 }, - { url = "https://files.pythonhosted.org/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726 }, - { url = "https://files.pythonhosted.org/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219 }, - { url = "https://files.pythonhosted.org/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, - { url = "https://files.pythonhosted.org/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733 }, - { url = "https://files.pythonhosted.org/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375 }, - { url = "https://files.pythonhosted.org/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307 }, - { url = "https://files.pythonhosted.org/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971 }, - { url = "https://files.pythonhosted.org/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616 }, - { url = "https://files.pythonhosted.org/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943 }, - { url = "https://files.pythonhosted.org/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654 }, - { url = "https://files.pythonhosted.org/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292 }, - { url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961 }, +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] name = "pydantic-settings" -version = "2.7.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pytest" -version = "8.3.3" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -390,127 +518,171 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-asyncio" -version = "0.24.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] name = "pytest-mock" -version = "3.14.0" +version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-benedict" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-fsutil" }, + { name = "python-slugify" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "useful-types" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/7f/670cea45a5de7ba79b820b9e58c19ec28070e65f8fe4584edc14b3b86ff2/python_benedict-0.35.0.tar.gz", hash = "sha256:ca825742cb60641939857417b799c6ca6680ba1ed7d3a92cf722103bc8dcb3ea", size = 58486, upload-time = "2025-09-30T22:57:42.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, + { url = "https://files.pythonhosted.org/packages/b6/ae/76f758e10b04c00f26c1613d8c77a078a3001d8ec439ec9e992d7c5a9268/python_benedict-0.35.0-py3-none-any.whl", hash = "sha256:4be33761812a2b8986b19f9dcb9df687feefd81c4cbb11becd044a5e28c340f6", size = 59905, upload-time = "2025-09-30T22:57:40.637Z" }, ] [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-fsutil" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/4a/494de3f8b079f077d687f7b3e32b963f7613eaae2d7b5c1be34d7eafd19a/python_fsutil-0.15.0.tar.gz", hash = "sha256:b51d8ab7ee218314480ea251fff7fef513be4fbccfe72a5af4ff2954f8a4a2c4", size = 29669, upload-time = "2025-02-06T17:47:55.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/de/fc2c3fa9d1f29c017c8eba2448efe86495b762111cf613a4c6d860158970/python_fsutil-0.15.0-py3-none-any.whl", hash = "sha256:8ae31def522916e35caf67723b8526fe6e5fcc1e160ea2dc23c845567708ca6e", size = 20915, upload-time = "2025-02-06T17:47:53.658Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, ] [[package]] name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, - { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, - { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, - { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, - { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, - { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, - { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, - { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "rapyuta-io-sdk-v2" -version = "0.0.1" source = { editable = "." } dependencies = [ { name = "httpx" }, - { name = "munch" }, { name = "pydantic-settings" }, + { name = "python-benedict" }, { name = "pyyaml" }, ] @@ -529,8 +701,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, - { name = "munch", specifier = ">=4.0.0" }, { name = "pydantic-settings", specifier = ">=2.7.1" }, + { name = "python-benedict", specifier = ">=0.34.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, ] @@ -546,29 +718,126 @@ dev = [ { name = "typing-extensions", specifier = ">=4.12.2" }, ] +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] [[package]] name = "tomli" -version = "2.0.2" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "useful-types" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/d6/0d6db1f8766e9b5e7ec259666c40ceae6bbb9326caf50e08717639e167b7/useful_types-0.2.1.tar.gz", hash = "sha256:870a0bcc8fcb7d0b2f14055438c1cab7e248fded942b0943a4d7019e7fbbdacd", size = 4748, upload-time = "2024-04-20T08:58:15.195Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/13/23/c194fd6c6258727f694e22b106c262c7d2678049dbc2d2045743e235f43a/useful_types-0.2.1-py3-none-any.whl", hash = "sha256:0dca32763d7271b5c8c7c395c44c10d09dba47a41aec97dcb085041ad096e0e9", size = 5382, upload-time = "2024-04-20T08:58:13.759Z" }, ]