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..cf1e86a 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 = "", @@ -74,7 +75,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 +120,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 +136,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..26f9b88 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) + + return Package(**result.json()) - @handle_and_munchify_response - async def delete_package(self, name: str, **kwargs) -> Munch: + 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,124 @@ 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() # -------------------Disks------------------- - @handle_and_munchify_response + async def list_disks( self, cont: int = 0, @@ -608,23 +700,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 +729,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 +818,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 +846,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 +897,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 +941,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 +975,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) - @handle_and_munchify_response - async def delete_network(self, name: str, **kwargs) -> Munch: + return Network(**result.json()) + + 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 +1043,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 +1124,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 +1293,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 +1315,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 +1328,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 +1348,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 +1373,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 +1387,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 +1409,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 +1447,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 +1455,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 +1489,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 +1512,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 +1532,7 @@ async def commit_revision( message: str = None, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Commit a revision. Args: @@ -1222,20 +1543,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 +1566,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 +1576,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 +1594,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 +1604,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 +1621,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 +1631,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 +1649,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 +1660,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 +1701,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 +1718,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 +1786,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 +1801,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 +1814,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 +1837,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 +1856,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..f5d1692 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,67 @@ 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) - 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, + 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 +361,58 @@ 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: + self, body: Project | dict, project_guid: str = None, **kwargs + ) -> 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( + if isinstance(body, dict): + body = Project.model_validate(body) + + result = 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 result # -------------------Package------------------- - @handle_and_munchify_response def list_packages( self, cont: int = 0, @@ -366,20 +420,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 +444,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 +476,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 +522,7 @@ def list_deployments( phases: list[str] = None, regions: list[str] = None, **kwargs, - ) -> Munch: + ) -> DeploymentList: """List all deployments in a project. Args: @@ -466,20 +530,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 +562,112 @@ 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 # -------------------Disks------------------- - @handle_and_munchify_response def list_disks( self, cont: int = 0, @@ -594,22 +677,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 +704,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 +788,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 +815,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 +863,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 +907,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 +941,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 +1005,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 +1082,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 +1252,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 +1288,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 +1307,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 +1331,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 +1345,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 +1366,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 +1400,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 +1408,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 +1439,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 +1460,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 +1478,7 @@ def commit_revision( message: str = None, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> dict[str, Any]: """Commit a revision. Args: @@ -1205,20 +1489,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 +1510,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 +1520,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 +1536,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 +1546,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 +1562,7 @@ def delete_key_in_revision( key: str, project_guid: str = None, **kwargs, - ) -> Munch: + ) -> None: """Delete a key in a revision. Args: @@ -1288,15 +1572,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 +1589,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 +1600,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 +1653,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 +1731,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 +1744,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 +1767,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 +1784,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..2d07c3b --- /dev/null +++ b/rapyuta_io_sdk_v2/models/deployment.py @@ -0,0 +1,259 @@ +""" +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 + + +class EnvArgsSpec(BaseModel): + name: str + value: str | None = None + + +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 = Field(default="always") + 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 + errorCode: str | None = Field(default=None, alias="error_code") + reason: str | None = None + restartCount: int | None = Field(default=None, alias="restart_count") # int32 in Go + exitCode: int | None = Field(default=None, alias="exit_code") # int32 in Go + + +class DependentDeploymentStatus(BaseModel): + name: str | None = None + guid: str | None = None + status: DeploymentStatusType | None = None + phase: DeploymentPhase | None = None + errorCodes: list[str] | None = Field(default=None, alias="error_codes") + + +class DependentNetworkStatus(BaseModel): + name: str | None = None + guid: str | None = None + status: DeploymentStatusType | None = None + phase: DeploymentPhase | None = None + errorCodes: list[str] | None = Field(default=None, alias="error_codes") + + +class DependentDiskStatus(BaseModel): + name: str | None = None + guid: str | None = None + status: str | None = None + errorCode: str | None = Field(default=None, alias="error_codes") + + +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 + errorCodes: list[str] | None = Field(default=None, alias="error_codes") + executablesStatus: dict[str, ExecutableStatus] | None = Field( + default=None, alias="executables_status" + ) + 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..11e8cf5 --- /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=1, ge=1) + timeoutSeconds: int | None = Field(default=10, ge=10) + periodSeconds: int | None = Field(default=1, ge=1) + successThreshold: int | None = Field(default=3, ge=1) + failureThreshold: int | None = Field(default=3, ge=1) + + +class EnvironmentSpec(BaseModel): + name: str + description: str | None = None + default: str | None = None + exposed: bool | None = Field(default=False) + 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=False) + scoped: bool | None = Field(default=False) + targeted: bool | None = Field(default=False) + 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..3685e88 --- /dev/null +++ b/rapyuta_io_sdk_v2/models/project.py @@ -0,0 +1,91 @@ +""" +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 + +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 | None = None + + +class FeaturesTracing(BaseModel): + enabled: bool | None = None + + +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/") + + +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..60c527c 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,199 @@ 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, 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/03/e3/873f433eca053c92d3cdb9336a379ee025bc1a86d4624ef87bf97a9ac7bc/asyncmock-0.4.2-py3-none-any.whl", hash = "sha256:fd8bc4e7813251a8959d1140924ccba3adbbc7af885dba7047c67f73c0b664b1", size = 4190 }, + { 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.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" 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/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } 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/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[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.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] [package.optional-dependencies] @@ -144,245 +235,226 @@ 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" 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/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 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/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.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/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 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/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "mock" -version = "5.1.0" +version = "5.2.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 } +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/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", size = 30938 }, -] - -[[package]] -name = "munch" -version = "4.0.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 } -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.11.9" 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/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } 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/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.2" 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/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] [[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 +462,169 @@ 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/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, + { 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.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-fsutil" }, + { name = "python-slugify" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/a8/744dce47aeba978a516f13c0d728fa1236217fa9e75e2619891df8100e85/python_benedict-0.34.1.tar.gz", hash = "sha256:0a81784c826c8983c485bb647ed5e6c8fad139c3921ea1844b21e41da0ff0dc0", size = 53340, upload-time = "2025-01-18T14:52:04.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/65/c59fb6eec8859047394ccd73eaf1623ce9fd82c48d880d9a072045669bc5/python_benedict-0.34.1-py3-none-any.whl", hash = "sha256:f09711ca2d8e716ca40440f1e26dfa3ccff15e08f1baa88de594fbba799cf87a", size = 51308, upload-time = "2025-01-18T14:52:01.886Z" }, ] [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.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/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } 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/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[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 +643,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 +660,104 @@ 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, 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/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { 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.2.1" 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/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 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/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[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/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.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.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/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/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { 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" }, ]