diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeff0db74..8756d5a39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,5 @@ name: Continuous Integration -env: - STORAGE_NAME: ${{ secrets.STORAGE_NAME }} - ACCOUNT_SECRET: ${{ secrets.ACCOUNT_SECRET }} - on: workflow_dispatch: push: @@ -55,13 +51,15 @@ jobs: with: enable-cache: true - - uses: azure/login@v3 + - name: Azure login + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - uses: azure/aks-set-context@v4 + - name: Azure AKS set context + uses: azure/aks-set-context@v4 with: cluster-name: "${{ vars.CLUSTER_NAME }}" resource-group: "${{ vars.CLUSTER_RESOURCE_GROUP }}" @@ -71,10 +69,12 @@ jobs: uv venv uv pip install . shell: bash + - name: Run integration tests working-directory: tests/integration/ run: ./test_api_endpoints.sh shell: bash + - name: End-to-end tests working-directory: tests/e2e/ run: ./test_e2e.sh diff --git a/Babylon/commands/macro/apply.py b/Babylon/commands/macro/apply.py index f27a74ed0..c0f9827fe 100644 --- a/Babylon/commands/macro/apply.py +++ b/Babylon/commands/macro/apply.py @@ -5,11 +5,11 @@ from click import argument, command, echo, option, style from yaml import safe_dump, safe_load -from Babylon.commands.macro.deploy import resolve_inclusion_exclusion from Babylon.commands.macro.deploy_organization import deploy_organization from Babylon.commands.macro.deploy_solution import deploy_solution from Babylon.commands.macro.deploy_webapp import deploy_webapp from Babylon.commands.macro.deploy_workspace import deploy_workspace +from Babylon.commands.macro.helpers.common import resolve_inclusion_exclusion from Babylon.utils.decorators import injectcontext from Babylon.utils.environment import Environment diff --git a/Babylon/commands/macro/deploy.py b/Babylon/commands/macro/deploy.py deleted file mode 100644 index f81846447..000000000 --- a/Babylon/commands/macro/deploy.py +++ /dev/null @@ -1,408 +0,0 @@ -import subprocess -from base64 import b64encode -from logging import getLogger -from textwrap import dedent - -from click import Abort, echo, style -from cosmotech_api.models.organization_access_control import OrganizationAccessControl -from cosmotech_api.models.organization_security import OrganizationSecurity -from cosmotech_api.models.solution_access_control import SolutionAccessControl -from cosmotech_api.models.solution_security import SolutionSecurity -from cosmotech_api.models.workspace_access_control import WorkspaceAccessControl -from cosmotech_api.models.workspace_security import WorkspaceSecurity -from kubernetes import client, config - -from Babylon.utils.environment import Environment - -logger = getLogger(__name__) - -env = Environment() - -# Helper functions for workspace deployment - - -def validate_inclusion_exclusion( - include: tuple[str], - exclude: tuple[str], -) -> bool: - """Include and exclude command line options cannot be combined and should have correct spelling""" - if include and exclude: # cannot combine conflicting options - echo(style("\n ✘ Argument Conflict", fg="red", bold=True)) - logger.error(" Cannot use [bold]--include[/bold] and [bold]--exclude[/bold] at the same time") - raise Abort() - - allowed_values = ("organization", "solution", "workspace", "webapp") - invalid_items = [i for i in include + exclude if i not in allowed_values] - if invalid_items: - echo(style("\n ✘ Invalid Arguments Detected", fg="red", bold=True)) - # List the errors - for item in invalid_items: - logger.error(f" • [yellow] {item}[/yellow] is not a valid resource type") - logger.error(f" Allowed values are: [cyan]{', '.join(allowed_values)}[/cyan]") - raise Abort() - return True - - -def resolve_inclusion_exclusion( - include: tuple[str], - exclude: tuple[str], -) -> tuple[bool, bool, bool]: - """Resolve command line include and exclude. - - Args: - include (tuple[str]): which objects to include in the deployment - exclude (tuple[str]): which objects to exclude from the deployment - - Raises: - ValueError: Error if incompatible options are provided - - Returns: - tuple[bool, bool, bool]: flags to include organization, solution, workspace - """ - validate_inclusion_exclusion(include, exclude) - organization = True - solution = True - workspace = True - webapp = True - if include: # if only is specified include by condition - organization = "organization" in include - solution = "solution" in include - workspace = "workspace" in include - webapp = "webapp" in include - if exclude: # if exclude is specified exclude by condition - organization = "organization" not in exclude - solution = "solution" not in exclude - workspace = "workspace" not in exclude - webapp = "webapp" not in exclude - return (organization, solution, workspace, webapp) - - -def diff( - acl1: OrganizationAccessControl | WorkspaceAccessControl | SolutionAccessControl, - acl2: OrganizationAccessControl | WorkspaceAccessControl | SolutionAccessControl, -) -> tuple[list[str], list[str], list[str]]: - """Generate a diff between two access control lists""" - ids1 = [i.id for i in acl1] - roles1 = [i.role for i in acl1] - ids2 = [i.id for i in acl2] - roles2 = [i.role for i in acl2] - to_add = [item for item in ids2 if item not in ids1] - to_delete = [item for item in ids1 if item not in ids2] - to_update = [item for item in ids1 if item in ids2 and roles1[ids1.index(item)] != roles2[ids2.index(item)]] - return (to_add, to_delete, to_update) - - -def update_default_security( - object_type: str, - current_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, - desired_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, - api_instance, - object_id: str, -) -> None: - if desired_security.default != current_security.default: - try: - getattr(api_instance, f"update_{object_type}_default_security")(object_id, desired_security.default) - logger.info(f" [bold green]✔[/bold green] Updated [magenta]{object_type}[/magenta] default security") - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to update [magenta]{object_type}[/magenta] default security: {e}") - - -def update_object_security( - object_type: str, - current_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, - desired_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, - api_instance, - object_id: list[str], -): - """Update object security: - if default security differs from payload - update object default security - diff state vs payload - foreach diff - delete entries to be removed - update entries to be changed - create entries to be added - """ - update_default_security(object_type, current_security, desired_security, api_instance, object_id) - (to_add, to_delete, to_update) = diff(current_security.access_control_list, desired_security.access_control_list) - for entry in desired_security.access_control_list: - if entry.id in to_add: - try: - getattr(api_instance, f"create_{object_type}_access_control")(*object_id, entry) - logger.info(f" [bold green]✔[/bold green] Access control for id [magenta]{entry.id}[/magenta] added successfully") - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to add access control for id [magenta]{entry.id}[/magenta]: {e}") - if entry.id in to_update: - try: - getattr(api_instance, f"update_{object_type}_access_control")(*object_id, entry.id, {"role": entry.role}) - logger.info(f" [bold green]✔[/bold green] Access control for id [magenta]{entry.id}[/magenta] updated successfully") - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to update access control for id [magenta]{entry.id}[/magenta]: {e}") - for entry_id in to_delete: - try: - getattr(api_instance, f"delete_{object_type}_access_control")(*object_id, entry_id) - logger.info(f" [bold green]✔[/bold green] Access control for id [magenta]{entry_id}[/magenta] deleted successfully") - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to delete access control for id [magenta]{entry_id}[/magenta]: {e}") - - -# Helper functions for workspace deployment - - -def get_postgres_service_host(namespace: str) -> str: - """Discovers the PostgreSQL service name in a namespace to build its FQDN - - Note: This function assumes PostgreSQL is running within the same Kubernetes cluster. - External database clusters are not currently supported. - """ - try: - config.load_kube_config() - v1 = client.CoreV1Api() - services = v1.list_namespaced_service(namespace) - - for svc in services.items: - if "postgresql" in svc.metadata.name or svc.metadata.labels.get("app.kubernetes.io/name") == "postgresql": - logger.info(f" [dim]→ Found PostgreSQL service {svc.metadata.name}[/dim]") - return f"{svc.metadata.name}.{namespace}.svc.cluster.local" - - return f"postgresql.{namespace}.svc.cluster.local" - except Exception as e: - logger.warning(" [bold yellow]⚠[/bold yellow] Service discovery failed ! default will be used.") - logger.debug(f" Exception details: {e}", exc_info=True) - return f"postgresql.{namespace}.svc.cluster.local" - - -def create_workspace_secret( - namespace: str, - organization_id: str, - workspace_id: str, - writer_password: str, -) -> bool: - """Create a Kubernetes Secret for a workspace containing API and PostgreSQL credentials. - - The secret is named ``-`` and holds all - environment variables required by workspace. - - Returns: - bool: True if the secret was created or already exists, False on error. - """ - secret_name = f"{organization_id}-{workspace_id}" - data = { - "POSTGRES_USER_PASSWORD": writer_password, - } - encoded_data = {k: b64encode(v.encode("utf-8")).decode("utf-8") for k, v in data.items()} - - secret = client.V1Secret( - api_version="v1", - kind="Secret", - metadata=client.V1ObjectMeta(name=secret_name, namespace=namespace), - type="Opaque", - data=encoded_data, - ) - - try: - config.load_kube_config() - v1 = client.CoreV1Api() - v1.create_namespaced_secret(namespace=namespace, body=secret) - logger.info(f" [bold green]✔[/bold green] Secret [magenta]{secret_name}[/magenta] created") - return True - except client.ApiException as e: - if e.status == 409: - logger.warning(f" [yellow]⚠[/yellow] [dim]Secret [magenta]{secret_name}[/magenta] already exists[/dim]") - return True - logger.error(f" [bold red]✘[/bold red] Failed to create secret {secret_name}: {e.reason}") - return False - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Unexpected error creating secret {secret_name}") - logger.debug(f" Detail: {e}", exc_info=True) - return False - - -def create_coal_configmap( - namespace: str, - organization_id: str, - workspace_id: str, - db_host: str, - db_port: str, - db_name: str, - schema_name: str, - writer_username: str, -) -> bool: - """Create a CoAL ConfigMap for a workspace. - - The ConfigMap is named ``--coal-config`` and - contains a ``coal-config.toml`` key with Postgres output configuration. The - ``user_password`` value is deliberately set to the literal string - ``env.POSTGRES_USER_PASSWORD`` so that the CoAL runtime resolves it from the - environment at execution time. - - Returns: - bool: True if the ConfigMap was created or already exists, False on error. - """ - configmap_name = f"{organization_id}-{workspace_id}-coal-config" - coal_toml = dedent(f"""\ - [[outputs]] - type = "postgres" - [outputs.conf.postgres] - host = "{db_host}" - port = "{db_port}" - db_name = "{db_name}" - db_schema = "{schema_name}" - user_name = "{writer_username}" - user_password = "env.POSTGRES_USER_PASSWORD" - """) - - configmap = client.V1ConfigMap( - api_version="v1", - kind="ConfigMap", - metadata=client.V1ObjectMeta(name=configmap_name, namespace=namespace), - data={"coal-config.toml": coal_toml}, - ) - - try: - config.load_kube_config() - v1 = client.CoreV1Api() - v1.create_namespaced_config_map(namespace=namespace, body=configmap) - logger.info(f" [bold green]✔[/bold green] ConfigMap [magenta]{configmap_name}[/magenta] created") - return True - except client.ApiException as e: - if e.status == 409: - logger.warning(f" [yellow]⚠[/yellow] [dim]ConfigMap [magenta]{configmap_name}[/magenta] already exists[/dim]") - return True - logger.error(f" [bold red]✘[/bold red] Failed to create ConfigMap {configmap_name}: {e.reason}") - return False - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Unexpected error creating ConfigMap {configmap_name}") - logger.debug(f" Detail: {e}", exc_info=True) - return False - - -def delete_kubernetes_resources(namespace: str, organization_id: str, workspace_id: str) -> None: - """Delete the Workspace Secret and CoAL ConfigMap created during deployment. - - Targets: - - Secret: ``-`` - - ConfigMap: ``--coal-config`` - - If a resource is already gone (404), a warning is logged and execution - continues without error. - """ - secret_name = f"{organization_id}-{workspace_id}" - configmap_name = f"{organization_id}-{workspace_id}-coal-config" - - try: - config.load_kube_config() - v1 = client.CoreV1Api() - except Exception as e: - logger.error(" [bold red]✘[/bold red] Failed to initialise Kubernetes client") - logger.debug(f" Detail: {e}", exc_info=True) - return - - # --- Delete Secret --- - try: - logger.info(" [dim]→ Deleting workspace Secret ...[/dim]") - v1.delete_namespaced_secret(name=secret_name, namespace=namespace) - logger.info(f" [bold green]✔[/bold green] Secret [magenta]{secret_name}[/magenta] deleted") - except client.ApiException as e: - if e.status == 404: - logger.warning(" [yellow]⚠[/yellow] [dim]Secret not found (already deleted)[/dim]") - else: - logger.error(f" [bold red]✘[/bold red] Failed to delete secret {secret_name}: {e.reason}") - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Unexpected error deleting secret {secret_name}") - logger.debug(f" Detail: {e}", exc_info=True) - - # --- Delete ConfigMap --- - try: - logger.info(" [dim]→ Deleting workspace ConfigMap ...[/dim]") - v1.delete_namespaced_config_map(name=configmap_name, namespace=namespace) - logger.info(f" [bold green]✔[/bold green] ConfigMap [magenta]{configmap_name}[/magenta] deleted") - except client.ApiException as e: - if e.status == 404: - logger.warning(" [yellow]⚠[/yellow] [dim]ConfigMap not found (already deleted)[/dim]") - else: - logger.error(f" [bold red]✘[/bold red] Failed to delete ConfigMap {configmap_name}: {e.reason}") - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Unexpected error deleting ConfigMap {configmap_name}") - logger.debug(f" Detail: {e}", exc_info=True) - - -# Helper functions for web application deployment - - -def dict_to_tfvars(payload: dict) -> str: - """Convert a dictionary to Terraform HCL tfvars format (key = "value"). - - Currently handles simple data structures: - - Booleans: converted to lowercase (true/false) - - Numbers: integers and floats as-is - - Strings: wrapped in double quotes - - Note: Complex nested structures (lists, dicts) are not yet supported. - This is sufficient for current WebApp tfvars which only use simple scalar values. - - Args: - payload (dict): Dictionary with simple key-value pairs - - Returns: - str: Terraform HCL formatted variable assignments - """ - lines = [] - for key, value in payload.items(): - if isinstance(value, bool): - lines.append(f"{key} = {str(value).lower()}") - elif isinstance(value, (int, float)): - lines.append(f"{key} = {value}") - else: - lines.append(f'{key} = "{value}"') - return "\n".join(lines) - - -def _run_terraform_process(executable, cwd, payload, state): - """Helper function to reduce the size of the main function (Clean Code)""" - try: - process = subprocess.Popen(executable, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) - - # Color mapping to avoid if/else statements in the loop - status_colors = { - "Initializing": "white", - "Upgrading": "white", - "Finding": "white", - "Refreshing": "white", - "Success": "green", - "complete": "green", - "Resources:": "green", - "Error": "red", - "error": "red", - } - - for line in process.stdout: - clean_line = line.strip() - if not clean_line: - continue - - color = next((status_colors[k] for k in status_colors if k in clean_line), "white") - is_bold = color == "red" - echo(style(f" {clean_line}", fg=color, bold=is_bold)) - - if process.wait() == 0: - _finalize_deployment(payload, state) - else: - logger.error(" [bold red]✘[/bold red] Deployment failed") - - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Execution error: {e}") - - -def _finalize_deployment(payload, state): - """Handles the update of the final state""" - webapp_name = payload.get("webapp_name") - url = f"https://{payload.get('cluster_name')}.{payload.get('domain_zone')}/tenant-{payload.get('tenant')}/webapp-{webapp_name}" - - services = state.setdefault("services", {}) - services["webapp"] = {"webapp_name": f"webapp-{webapp_name}", "webapp_url": url} - - logger.info(f" [bold green]✔[/bold green] WebApp [bold white]{webapp_name}[/bold white] deployed") - env.store_state_in_local(state) - if env.remote: - env.store_state_in_cloud(state) diff --git a/Babylon/commands/macro/deploy_organization.py b/Babylon/commands/macro/deploy_organization.py index 43589be95..47d47e1dc 100644 --- a/Babylon/commands/macro/deploy_organization.py +++ b/Babylon/commands/macro/deploy_organization.py @@ -7,7 +7,7 @@ from cosmotech_api.models.organization_update_request import OrganizationUpdateRequest from Babylon.commands.api.organization import get_organization_api_instance -from Babylon.commands.macro.deploy import update_object_security +from Babylon.commands.macro.helpers.common import update_object_security from Babylon.utils.credentials import get_keycloak_token from Babylon.utils.environment import Environment from Babylon.utils.response import CommandResponse @@ -77,4 +77,4 @@ def deploy_organization(namespace: str, file_content: str): # Ensure the local and remote states are synchronized after successful API calls env.store_state_in_local(state) if env.remote: - env.store_state_in_cloud(state) + env.store_state_in_kubernetes(state) diff --git a/Babylon/commands/macro/deploy_solution.py b/Babylon/commands/macro/deploy_solution.py index bf004d55c..5caea2bf3 100644 --- a/Babylon/commands/macro/deploy_solution.py +++ b/Babylon/commands/macro/deploy_solution.py @@ -7,7 +7,7 @@ from cosmotech_api.models.solution_update_request import SolutionUpdateRequest from Babylon.commands.api.solution import get_solution_api_instance -from Babylon.commands.macro.deploy import update_object_security +from Babylon.commands.macro.helpers.common import update_object_security from Babylon.utils.credentials import get_keycloak_token from Babylon.utils.environment import Environment from Babylon.utils.response import CommandResponse @@ -83,4 +83,4 @@ def deploy_solution(namespace: str, file_content: str) -> bool: # Ensure the local and remote states are synchronized after successful API calls env.store_state_in_local(state) if env.remote: - env.store_state_in_cloud(state) + env.store_state_in_kubernetes(state) diff --git a/Babylon/commands/macro/deploy_webapp.py b/Babylon/commands/macro/deploy_webapp.py index 3b525d511..f6cbb7003 100644 --- a/Babylon/commands/macro/deploy_webapp.py +++ b/Babylon/commands/macro/deploy_webapp.py @@ -5,7 +5,7 @@ from click import echo, style -from Babylon.commands.macro.deploy import _run_terraform_process, dict_to_tfvars +from Babylon.commands.macro.helpers.webapp import dict_to_tfvars, run_terraform_process from Babylon.utils.environment import Environment logger = getLogger(__name__) @@ -47,7 +47,7 @@ def deploy_webapp(namespace: str, file_content: str): if content != updated_content: script_path.write_text(updated_content) if sys.platform == "linux": - os.chmod(script_path, 0o755) + os.chmod(script_path, 0o700) except IOError as e: logger.error(f" [bold red]✘[/bold red] Script modification failed: {e}") return @@ -59,4 +59,4 @@ def deploy_webapp(namespace: str, file_content: str): return logger.info(" [dim]→ Running Terraform deployment...[/dim]") - _run_terraform_process(executable, tf_dir, payload, state) + run_terraform_process(executable, tf_dir, payload, state) diff --git a/Babylon/commands/macro/deploy_workspace.py b/Babylon/commands/macro/deploy_workspace.py index 5afdc6415..a502dfffe 100644 --- a/Babylon/commands/macro/deploy_workspace.py +++ b/Babylon/commands/macro/deploy_workspace.py @@ -1,24 +1,13 @@ -import subprocess -from json import dumps from logging import getLogger from pathlib import Path as PathlibPath -from string import Template from click import echo, style -from cosmotech_api.models.workspace_create_request import WorkspaceCreateRequest -from cosmotech_api.models.workspace_security import WorkspaceSecurity -from cosmotech_api.models.workspace_update_request import WorkspaceUpdateRequest -from kubernetes import client, utils -from kubernetes import config as kube_config -from kubernetes.utils import FailToCreateError -from yaml import safe_load from Babylon.commands.api.workspace import get_workspace_api_instance -from Babylon.commands.macro.deploy import ( - create_coal_configmap, - create_workspace_secret, - get_postgres_service_host, - update_object_security, +from Babylon.commands.macro.helpers.workspace import ( + create_workspace, + deploy_postgres_schema, + update_workspace, ) from Babylon.utils.credentials import get_keycloak_token from Babylon.utils.environment import Environment @@ -31,195 +20,32 @@ def deploy_workspace(namespace: str, file_content: str, deploy_dir: PathlibPath) -> bool: echo(style(f"\n🚀 Deploying Workspace in namespace: {env.environ_id}", bold=True, fg="cyan")) - # Retrieve the state env.get_ns_from_text(content=namespace) state = env.retrieve_state_func() content = env.fill_template(data=file_content, state=state) - # Authentication and API client initialization keycloak_token, config = get_keycloak_token() payload: dict = content.get("spec").get("payload") api_section = state["services"]["api"] - # Determine if we are performing a Create or Update based on state api_section["workspace_id"] = payload.get("id") or api_section.get("workspace_id", "") - spec = {} - spec["payload"] = dumps(payload, indent=2, ensure_ascii=True) api_instance = get_workspace_api_instance(config=config, keycloak_token=keycloak_token) # --- Deployment Logic --- if not api_section["workspace_id"]: - # Case: New Workspace - logger.info(" [dim]→ No existing workspace ID found. Creating...[/dim]") - workspace_create_request = WorkspaceCreateRequest.from_dict(payload) - workspace = api_instance.create_workspace( - organization_id=api_section["organization_id"], workspace_create_request=workspace_create_request - ) - if workspace is None: - logger.error(" [bold red]✘[/bold red] Failed to create workspace") + if not create_workspace(api_instance, api_section, payload, state): return CommandResponse.fail() - # Save the newly generated ID to state - logger.info(f" [bold green]✔[/bold green] Workspace [bold magenta]{workspace.id}[/bold magenta] created") - state["services"]["api"]["workspace_id"] = workspace.id else: - # Case: Update Existing Workspace - logger.info(f" [dim]→ Existing ID [bold cyan]{api_section['workspace_id']}[/bold cyan] found. Updating...[/dim]") - workspace_update_request = WorkspaceUpdateRequest.from_dict(payload) - updated = api_instance.update_workspace( - organization_id=api_section["organization_id"], - workspace_id=api_section["workspace_id"], - workspace_update_request=workspace_update_request, - ) - if updated is None: - logger.error(f" [bold red]✘[/bold red] Failed to update workspace {api_section['workspace_id']}") + if not update_workspace(api_instance, api_section, payload): return CommandResponse.fail() - # Handle Security Policy synchronization if provided in payload - if payload.get("security"): - try: - logger.info(" [dim]→ Syncing security policies...[/dim]") - current_security = api_instance.get_workspace_security( - organization_id=api_section["organization_id"], workspace_id=api_section["workspace_id"] - ) - update_object_security( - "workspace", - current_security=current_security, - desired_security=WorkspaceSecurity.from_dict(payload.get("security")), - api_instance=api_instance, - object_id=[api_section["organization_id"], api_section["workspace_id"]], - ) - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Security update failed: {e}") - return CommandResponse.fail() - logger.info(f" [bold green]✔[/bold green] Workspace [bold magenta]{api_section['workspace_id']}[/bold magenta] updated") + + # --- PostgreSQL Schema --- workspace_id = state["services"]["api"]["workspace_id"] spec = content.get("spec") or {} - sidecars = spec.get("sidecars") or {} - postgres_section = sidecars.get("postgres") or {} - schema_config = postgres_section.get("schema") or {} - should_create_schema = schema_config.get("create", False) - if should_create_schema: - db_host = get_postgres_service_host(env.environ_id) - logger.info(f" [dim]→ Initializing PostgreSQL schema for workspace [bold cyan]{workspace_id}[/bold cyan]...[/dim]") - pg_config = env.get_config_from_k8s_secret_by_tenant("postgresql-config", env.environ_id) - api_config = env.get_config_from_k8s_secret_by_tenant("postgresql-cosmotechapi", env.environ_id) - if pg_config and api_config: - schema_name = f"{workspace_id.replace('-', '_')}" - mapping = { - "namespace": env.environ_id, - "db_host": db_host, - "db_port": "5432", - "cosmotech_api_database": api_config.get("database-name", ""), - "cosmotech_api_admin_username": api_config.get("admin-username", ""), - "cosmotech_api_admin_password": api_config.get("admin-password", ""), - "cosmotech_api_writer_username": api_config.get("writer-username", ""), - "cosmotech_api_reader_username": api_config.get("reader-username", ""), - "workspace_schema": schema_name, - "job_name": workspace_id, - } - jobs = schema_config.get("jobs", []) - if not isinstance(deploy_dir, PathlibPath): - deploy_dir = PathlibPath(deploy_dir) - for job in jobs: - script_path = deploy_dir / job.get("path", "") / job.get("name", "") - if script_path.exists(): - kube_config.load_kube_config() - k8s_client = client.ApiClient() - k8s_job_name = f"postgresql-init-{workspace_id}" - with open(script_path, "r") as f: - raw_content = f.read() - templated_yaml = Template(raw_content).safe_substitute(mapping) - yaml_dict = safe_load(templated_yaml) - try: - utils.create_from_dict(k8s_client, yaml_dict, namespace=env.environ_id) - logger.info(f" [dim]→ Waiting for job [cyan]{k8s_job_name}[/cyan] to complete...[/dim]") - wait_process = subprocess.run( - [ - "kubectl", - "wait", - "--for=condition=complete", - "job", - k8s_job_name, - f"--namespace={env.environ_id}", - "--timeout=50s", - ], - capture_output=True, - text=True, - ) - if wait_process.returncode != 0: - logger.error( - f" [bold red]✘[/bold red] Job {k8s_job_name} did not complete successfully" - f" see babylon logs for details" - ) - logger.debug(f" [bold red]✘[/bold red] Job wait output {wait_process.stdout} {wait_process.stderr}") - else: - # Job completed, now check the logs for error - logger.info(" [dim]→ Checking job logs for errors...[/dim]") - logs_process = subprocess.run( - ["kubectl", "logs", f"job/{k8s_job_name}", "-n", env.environ_id], - capture_output=True, - text=True, - ) - if logs_process.returncode == 0: - job_logs = logs_process.stdout if logs_process.stdout else logs_process.stderr - if "ERROR" in job_logs or "error" in job_logs: - logger.error(" [bold red]✘[/bold red] Schema creation failed inside the container") - logger.debug(f" [bold red]✘[/bold red] Job logs : {job_logs}") - elif "already exists" in job_logs: - logger.info( - f" [yellow]⚠[/yellow] [dim]Schema [magenta]{schema_name}[/magenta]" - f" already exists (skipping creation)[/dim]" - ) - else: - logger.info( - f" [green]✔[/green] Schema creation [magenta]{schema_name}[/magenta] completed successfully" - ) - state["services"]["postgres"]["schema_name"] = schema_name - else: - logger.error(f" [bold red]✘[/bold red] Failed to retrieve logs for job {k8s_job_name}") - logger.debug( - f" [bold red]✘[/bold red] Logs retrieval output {logs_process.stdout} {logs_process.stderr}" - ) - - except FailToCreateError as e: - for inner_exception in e.api_exceptions: - if inner_exception.status == 409: - logger.warning(f" [yellow]⚠[/yellow] [dim]Job [cyan]{k8s_job_name}[/cyan] already exists.[/dim]") - else: - logger.error( - f" [bold red]✘[/bold red] K8s Error ({inner_exception.status}): {inner_exception.reason}" - ) - logger.debug(f" Detail: {inner_exception.body}") - except Exception as e: - logger.error(" [bold red]✘[/bold red] Unexpected error please check babylon logs file for details") - logger.debug(f" [bold red]✘[/bold red] {e}") - - # --- Workspace Secret & CoAL ConfigMap --- - organization_id = api_section["organization_id"] - writer_username = api_config.get("writer-username", "") - writer_password = api_config.get("writer-password", "") - db_name = api_config.get("database-name", "") - - logger.info(f" [dim]→ Creating workspace secret for [cyan]{workspace_id}[/cyan]...[/dim]") - create_workspace_secret( - namespace=env.environ_id, - organization_id=organization_id, - workspace_id=workspace_id, - writer_password=writer_password, - ) - - logger.info(f" [dim]→ Creating CoAL ConfigMap for [cyan]{workspace_id}[/cyan]...[/dim]") - create_coal_configmap( - namespace=env.environ_id, - organization_id=organization_id, - workspace_id=workspace_id, - db_host=db_host, - db_port="5432", - db_name=db_name, - schema_name=schema_name, - writer_username=writer_username, - ) + schema_config = spec.get("sidecars", {}).get("postgres", {}).get("schema") or {} + if schema_config.get("create", False): + deploy_postgres_schema(workspace_id, schema_config, api_section, deploy_dir, state) # --- State Persistence --- - # Ensure the local and remote states are synchronized after successful API calls env.store_state_in_local(state) if env.remote: - env.store_state_in_cloud(state) + env.store_state_in_kubernetes(state) diff --git a/Babylon/commands/macro/destroy.py b/Babylon/commands/macro/destroy.py index fa35d10b2..7e18b583d 100644 --- a/Babylon/commands/macro/destroy.py +++ b/Babylon/commands/macro/destroy.py @@ -1,18 +1,17 @@ -import subprocess from logging import getLogger -from pathlib import Path -from string import Template -from typing import Callable from click import command, echo, option, style -from kubernetes import client, utils -from kubernetes import config as kube_config -from yaml import safe_load from Babylon.commands.api.organization import get_organization_api_instance from Babylon.commands.api.solution import get_solution_api_instance from Babylon.commands.api.workspace import get_workspace_api_instance -from Babylon.commands.macro.deploy import delete_kubernetes_resources, get_postgres_service_host, resolve_inclusion_exclusion +from Babylon.commands.macro.helpers.common import resolve_inclusion_exclusion +from Babylon.commands.macro.helpers.webapp import destroy_webapp +from Babylon.commands.macro.helpers.workspace import ( + delete_api_resource, + delete_kubernetes_resources, + destroy_postgres_schema, +) from Babylon.utils.credentials import get_keycloak_token from Babylon.utils.decorators import injectcontext, retrieve_state from Babylon.utils.environment import Environment @@ -22,173 +21,6 @@ env = Environment() -def _destroy_schema(schema_name: str, state: dict) -> None: - """ - Destroy PostgreSQL schema for a workspace. - """ - if not schema_name: - logger.warning(" [yellow]⚠[/yellow] [dim]No schema found ! skipping deletion[/dim]") - return - workspace_id_tmp = f"{schema_name.replace('_', '-')}" - db_host = get_postgres_service_host(env.environ_id) - logger.info(f" [dim]→ Destroying postgreSQL schema for workspace [bold cyan]{workspace_id_tmp}[/bold cyan]...[/dim]") - - pg_config = env.get_config_from_k8s_secret_by_tenant("postgresql-config", env.environ_id) - api_config = env.get_config_from_k8s_secret_by_tenant("postgresql-cosmotechapi", env.environ_id) - - if not pg_config or not api_config: - logger.error(" [bold red]✘[/bold red] Failed to retrieve postgreSQL configuration from secrets") - return - - mapping = { - "namespace": env.environ_id, - "db_host": db_host, - "db_port": "5432", - "cosmotech_api_database": api_config.get("database-name"), - "cosmotech_api_admin_username": api_config.get("admin-username"), - "cosmotech_api_admin_password": api_config.get("admin-password"), - "cosmotech_api_writer_username": api_config.get("writer-username"), - "cosmotech_api_reader_username": api_config.get("reader-username"), - "workspace_schema": schema_name, - "job_name": workspace_id_tmp, - } - destroy_jobs = env.original_template_path / "yaml" / "k8s_job_destroy.yaml" - k8s_job_name = f"postgresql-destroy-{workspace_id_tmp}" - kube_config.load_kube_config() - k8s_client = client.ApiClient() - with open(destroy_jobs, "r") as f: - raw_content = f.read() - - templated_yaml = Template(raw_content).safe_substitute(mapping) - yaml_dict = safe_load(templated_yaml) - logger.info(" [dim]→ Applying kubernetes destroy job...[/dim]") - try: - utils.create_from_dict(k8s_client, yaml_dict, namespace=env.environ_id) - logger.info(f" [dim]→ Waiting for job [cyan]{k8s_job_name}[/cyan] to complete...[/dim]") - wait_process = subprocess.run( - [ - "kubectl", - "wait", - "--for=condition=complete", - "job", - k8s_job_name, - f"--namespace={env.environ_id}", - "--timeout=300s", - ], - capture_output=True, - text=True, - ) - if wait_process.returncode == 0: - # Job completed, now check the logs for error - logger.info(" [dim]→ Checking job logs for errors...[/dim]") - logs_process = subprocess.run( - ["kubectl", "logs", f"job/{k8s_job_name}", "-n", env.environ_id], - capture_output=True, - text=True, - ) - if logs_process.returncode == 0: - job_logs = logs_process.stdout if logs_process.stdout else logs_process.stderr - if "ERROR" in job_logs or "error" in job_logs: - logger.error(" [bold red]✘[/bold red] Schema destruction failed inside the container") - logger.debug(f" [bold red]✘[/bold red] Job logs : {job_logs}") - elif "does not exist" in job_logs: - logger.info( - f" [yellow]⚠[/yellow] [dim]Schema [magenta]{schema_name}[/magenta] does not exist (nothing to clean)[/dim]" - ) - state["services"]["postgres"]["schema_name"] = "" - else: - logger.info(f" [green]✔[/green] Schema destruction [magenta]{schema_name}[/magenta] completed successfully") - state["services"]["postgres"]["schema_name"] = "" - else: - logger.error(f" [bold red]✘[/bold red] Failed to retrieve logs for job {k8s_job_name}") - logger.debug(f" [bold red]✘[/bold red] Logs retrieval output {logs_process.stdout} {logs_process.stderr}") - - else: - logger.error(f" [bold red]✘[/bold red] Job {k8s_job_name} did not complete successfully see babylon logs for details") - logger.debug(f" [bold red]✘[/bold red] Job wait output {wait_process.stdout} {wait_process.stderr}") - - except Exception as e: - logger.error(" [bold red]✘[/bold red] Unexpected error please check babylon logs file for details") - logger.debug(f" [bold red]✘[/bold red] {e}") - - -def _destroy_webapp(state: dict): - """Terraform Destroy webapp""" - logger.info(" [dim]→ Running Terraform destroy for WebApp resources...[/dim]") - webapp_state = state.get("services", {}).get("webapp", {}) - webapp_neme = webapp_state.get("webapp_name") - if not webapp_neme: - logger.warning(" [yellow]⚠[/yellow] [dim]No WebApp found in state! skipping deletion [dim]") - return - tf_dir = Path(str(env.working_dir)).parent / "terraform-webapp" - - if not tf_dir.exists(): - logger.error(f" [bold red]✘[/bold red] Terraform directory not found at {tf_dir}") - return - try: - process = subprocess.Popen( - ["terraform", "destroy", "-auto-approve"], - cwd=tf_dir, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - ) - - line_handlers = { - "Destroy complete!": "green", - "Resources:": "green", - "Error": "red", - } - - for line in process.stdout: - clean_line = line.strip() - if not clean_line: - continue - - color = next((color for key, color in line_handlers.items() if key in clean_line), "white") - bold = color == "red" - echo(style(f" {clean_line}", fg=color, bold=bold)) - - process.wait() - if process.returncode == 0: - # Nettoyage du state webapp - state["services"]["webapp"]["webapp_name"] = "" - state["services"]["webapp"]["webapp_url"] = "" - logger.info(f" [green]✔[/green] WebApp [magenta]{webapp_neme}[/magenta] destroyed") - else: - logger.error(f" [bold red]✘[/bold red] Terraform destroy failed (Code {process.returncode})") - - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Error during WebApp destruction: {e}") - - -def _delete_resource( - api_call: Callable[..., None], resource_name: str, org_id: str | None, resource_id: str, state: dict, state_key: str -): - """Helper to handle repetitive deletion logic and error handling.""" - if not resource_id: - logger.warning(f" [yellow]⚠[/yellow] [dim]No {resource_name} ID found in state! skipping deletion[dim]") - return - - try: - logger.info(f" [dim]→ Existing ID [bold cyan]{resource_id}[/bold cyan] found. Deleting...[/dim]") - if org_id and resource_name != "Organization": - api_call(organization_id=org_id, **{f"{resource_name.lower()}_id": resource_id}) - else: - api_call(organization_id=resource_id) - - logger.info(f" [bold green]✔[/bold green] {resource_name} [magenta]{resource_id}[/magenta] deleted") - state["services"]["api"][state_key] = "" - except Exception as e: - error_msg = str(e) - if "404" in error_msg or "Not Found" in error_msg: - logger.info(f" [bold yellow]⚠[/bold yellow] {resource_name} [magenta]{resource_id}[/magenta] already deleted (404)") - state["services"]["api"][state_key] = "" - else: - logger.error(f" [bold red]✘[/bold red] Error deleting {resource_name.lower()} {resource_id} reason: {e}") - - @command() @injectcontext() @retrieve_state @@ -197,42 +29,39 @@ def _delete_resource( def destroy(state: dict, include: tuple[str], exclude: tuple[str]): """Macro Destroy""" organization, solution, workspace, webapp = resolve_inclusion_exclusion(include, exclude) - # Header for the destructive operation echo(style(f"\n🔥 Starting Destruction Process in namespace: {env.environ_id}", bold=True, fg="red")) keycloak_token, config = get_keycloak_token() - # We need the Org ID for most sub-resource deletions api_state = state["services"]["api"] schema_state = state["services"]["postgres"] org_id = api_state["organization_id"] if solution: api = get_solution_api_instance(config=config, keycloak_token=keycloak_token) - _delete_resource(api.delete_solution, "Solution", org_id, api_state["solution_id"], state, "solution_id") + delete_api_resource(api.delete_solution, "Solution", org_id, api_state["solution_id"], state, "solution_id") if workspace: - _destroy_schema(schema_state["schema_name"], state) + destroy_postgres_schema(schema_state["schema_name"], state) delete_kubernetes_resources( namespace=env.environ_id, organization_id=org_id, workspace_id=api_state["workspace_id"], ) api = get_workspace_api_instance(config=config, keycloak_token=keycloak_token) - _delete_resource(api.delete_workspace, "Workspace", org_id, api_state["workspace_id"], state, "workspace_id") + delete_api_resource(api.delete_workspace, "Workspace", org_id, api_state["workspace_id"], state, "workspace_id") if organization: api = get_organization_api_instance(config=config, keycloak_token=keycloak_token) - _delete_resource(api.delete_organization, "Organization", None, org_id, state, "organization_id") + delete_api_resource(api.delete_organization, "Organization", None, org_id, state, "organization_id") if webapp: - _destroy_webapp(state) + destroy_webapp(state) # --- State Persistence --- env.store_state_in_local(state=state) if state.get("remote"): - logger.info(" [dim]☁ Syncing state cleanup to cloud...[/dim]") - env.set_blob_client() - env.store_state_in_cloud(state=state) + logger.info(" [dim]☁ Syncing state cleanup to kubernetes...[/dim]") + env.store_state_in_kubernetes(state=state) # --- Final Destruction Summary --- echo(style("\n📋 Destruction Summary", bold=True, fg="white")) @@ -240,14 +69,11 @@ def destroy(state: dict, include: tuple[str], exclude: tuple[str]): services = final_state.get("services") api_data = services.get("api") for key, value in api_data.items(): - # Prepare the label (e.g., "Organization Id") label_text = f" • {key.replace('_', ' ').title()}" - # We check if the ID is now empty (which means it was deleted) status = "DELETED" if not value else value color = "red" if status == "DELETED" else "green" echo(f"{style(f'{label_text:<20}:', fg='cyan', bold=True)} {style(status, fg=color)}") - # Affichage WebApp webapp_data = services.get("webapp", {}) webapp_id = webapp_data.get("webapp_name") label_text = " • Webapp Name" diff --git a/Babylon/commands/macro/helpers/__init__.py b/Babylon/commands/macro/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Babylon/commands/macro/helpers/common.py b/Babylon/commands/macro/helpers/common.py new file mode 100644 index 000000000..6a7e6624b --- /dev/null +++ b/Babylon/commands/macro/helpers/common.py @@ -0,0 +1,140 @@ +""" +Shared helpers used across all macro commands. + +Covers: +- ``include`` / ``exclude`` CLI option validation and resolution +- ACL diff computation +- Generic object-security synchronisation (organization, solution, workspace) +""" + +from logging import getLogger + +from click import Abort, echo, style +from cosmotech_api.models.organization_access_control import OrganizationAccessControl +from cosmotech_api.models.organization_security import OrganizationSecurity +from cosmotech_api.models.solution_access_control import SolutionAccessControl +from cosmotech_api.models.solution_security import SolutionSecurity +from cosmotech_api.models.workspace_access_control import WorkspaceAccessControl +from cosmotech_api.models.workspace_security import WorkspaceSecurity + +logger = getLogger(__name__) + + +def validate_inclusion_exclusion( + include: tuple[str], + exclude: tuple[str], +) -> bool: + """Include and exclude command line options cannot be combined and should have correct spelling.""" + if include and exclude: # cannot combine conflicting options + echo(style("\n ✘ Argument Conflict", fg="red", bold=True)) + logger.error(" Cannot use [bold]--include[/bold] and [bold]--exclude[/bold] at the same time") + raise Abort() + + allowed_values = ("organization", "solution", "workspace", "webapp") + invalid_items = [i for i in include + exclude if i not in allowed_values] + if invalid_items: + echo(style("\n ✘ Invalid Arguments Detected", fg="red", bold=True)) + for item in invalid_items: + logger.error(f" • [yellow] {item}[/yellow] is not a valid resource type") + logger.error(f" Allowed values are: [cyan]{', '.join(allowed_values)}[/cyan]") + raise Abort() + return True + + +def resolve_inclusion_exclusion( + include: tuple[str], + exclude: tuple[str], +) -> tuple[bool, bool, bool, bool]: + """Resolve command line include and exclude. + + Args: + include (tuple[str]): which objects to include in the deployment + exclude (tuple[str]): which objects to exclude from the deployment + + Raises: + Abort: if incompatible or invalid options are provided + + Returns: + tuple[bool, bool, bool, bool]: flags to include organization, solution, workspace, webapp + """ + validate_inclusion_exclusion(include, exclude) + organization = True + solution = True + workspace = True + webapp = True + if include: # if only is specified include by condition + organization = "organization" in include + solution = "solution" in include + workspace = "workspace" in include + webapp = "webapp" in include + if exclude: # if exclude is specified exclude by condition + organization = "organization" not in exclude + solution = "solution" not in exclude + workspace = "workspace" not in exclude + webapp = "webapp" not in exclude + return (organization, solution, workspace, webapp) + + +def diff( + acl1: OrganizationAccessControl | WorkspaceAccessControl | SolutionAccessControl, + acl2: OrganizationAccessControl | WorkspaceAccessControl | SolutionAccessControl, +) -> tuple[list[str], list[str], list[str]]: + """Generate a diff between two access control lists.""" + ids1 = [i.id for i in acl1] + roles1 = [i.role for i in acl1] + ids2 = [i.id for i in acl2] + roles2 = [i.role for i in acl2] + to_add = [item for item in ids2 if item not in ids1] + to_delete = [item for item in ids1 if item not in ids2] + to_update = [item for item in ids1 if item in ids2 and roles1[ids1.index(item)] != roles2[ids2.index(item)]] + return (to_add, to_delete, to_update) + + +def update_default_security( + object_type: str, + current_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, + desired_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, + api_instance, + object_id: str, +) -> None: + if desired_security.default != current_security.default: + try: + getattr(api_instance, f"update_{object_type}_default_security")(object_id, desired_security.default) + logger.info(f" [bold green]✔[/bold green] Updated [magenta]{object_type}[/magenta] default security") + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Failed to update [magenta]{object_type}[/magenta] default security: {e}") + + +def update_object_security( + object_type: str, + current_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, + desired_security: OrganizationSecurity | WorkspaceSecurity | SolutionSecurity, + api_instance, + object_id: list[str], +) -> None: + """Update object security: + - if default security differs from payload, update object default security + - diff state vs payload + - foreach diff: delete entries to be removed, update entries to be changed, create entries to be added + """ + update_default_security(object_type, current_security, desired_security, api_instance, object_id) + (to_add, to_delete, to_update) = diff(current_security.access_control_list, desired_security.access_control_list) + for entry in desired_security.access_control_list: + if entry.id in to_add: + try: + getattr(api_instance, f"create_{object_type}_access_control")(*object_id, entry) + logger.info(f" [bold green]✔[/bold green] Access control for id [magenta]{entry.id}[/magenta] added successfully") + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Failed to add access control for id [magenta]{entry.id}[/magenta]: {e}") + if entry.id in to_update: + try: + getattr(api_instance, f"update_{object_type}_access_control")(*object_id, entry.id, {"role": entry.role}) + logger.info(f" [bold green]✔[/bold green] Access control for id [magenta]{entry.id}[/magenta] updated successfully") + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Failed to update access control for id [magenta]{entry.id}[/magenta]: {e}") + for entry_id in to_delete: + try: + getattr(api_instance, f"delete_{object_type}_access_control")(*object_id, entry_id) + logger.info(f" [bold green]✔[/bold green] Access control for id [magenta]{entry_id}[/magenta] deleted successfully") + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Failed to delete access control for id [magenta]{entry_id}[/magenta]: {e}") diff --git a/Babylon/commands/macro/helpers/webapp.py b/Babylon/commands/macro/helpers/webapp.py new file mode 100644 index 000000000..409316b9f --- /dev/null +++ b/Babylon/commands/macro/helpers/webapp.py @@ -0,0 +1,168 @@ +""" +Terraform helpers for WebApp deployment and teardown. + +Covers: +- Converting a payload dict to Terraform HCL tfvars format +- Running a Terraform subprocess and streaming its output +- Finalising state after a successful Terraform apply +- Destroying WebApp infrastructure via Terraform +""" + +import subprocess +from logging import getLogger +from pathlib import Path +from sys import exit + +from click import echo, style + +from Babylon.utils.environment import Environment + +logger = getLogger(__name__) +env = Environment() + + +def dict_to_tfvars(payload: dict) -> str: + """Convert a dictionary to Terraform HCL tfvars format (key = "value"). + + Currently handles simple data structures: + - Booleans: converted to lowercase (true/false) + - Numbers: integers and floats as-is + - Strings: wrapped in double quotes + + Note: Complex nested structures (lists, dicts) are not yet supported. + This is sufficient for current WebApp tfvars which only use simple scalar values. + + Args: + payload (dict): Dictionary with simple key-value pairs + + Returns: + str: Terraform HCL formatted variable assignments + """ + lines = [] + for key, value in payload.items(): + if isinstance(value, bool): + lines.append(f"{key} = {str(value).lower()}") + elif isinstance(value, (int, float)): + lines.append(f"{key} = {value}") + else: + lines.append(f'{key} = "{value}"') + return "\n".join(lines) + + +def _finalize_deployment(payload: dict, state: dict) -> None: + """Update state with the final WebApp name and URL after a successful Terraform apply.""" + webapp_name = payload.get("webapp_name") + url = f"https://{payload.get('cluster_name')}.{payload.get('domain_zone')}/tenant-{payload.get('tenant')}/webapp-{webapp_name}" + + services = state.setdefault("services", {}) + services["webapp"] = {"webapp_name": f"webapp-{webapp_name}", "webapp_url": url} + + logger.info(f" [bold green]✔[/bold green] WebApp [bold white]{webapp_name}[/bold white] deployed") + env.store_state_in_local(state) + if env.remote: + env.store_state_in_kubernetes(state) + + +def run_terraform_process(executable: list[str], cwd, payload: dict, state: dict) -> None: + """Stream a Terraform subprocess and finalize state on success. + + Args: + executable: The command + arguments to run (e.g. ``['/bin/bash', './_run-terraform.sh']``). + cwd: Working directory for the subprocess (path to the Terraform directory). + payload: The WebApp payload dict, forwarded to ``_finalize_deployment``. + state: Current Babylon state dict, forwarded to ``_finalize_deployment``. + """ + try: + process = subprocess.Popen(executable, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) + + # Color mapping to avoid if/else statements in the loop + status_colors = { + "Initializing": "white", + "Upgrading": "white", + "Finding": "white", + "Refreshing": "white", + "Success": "green", + "complete": "green", + "Resources:": "green", + "Error": "red", + "error": "red", + } + + for line in process.stdout: + clean_line = line.strip() + if not clean_line: + continue + + color = next((status_colors[k] for k in status_colors if k in clean_line), "white") + is_bold = color == "red" + echo(style(f" {clean_line}", fg=color, bold=is_bold)) + if "Error" in clean_line or "error" in clean_line: + exit(1) + + if process.wait() == 0: + _finalize_deployment(payload, state) + else: + logger.error(" [bold red]✘[/bold red] Deployment failed") + + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Execution error: {e}") + + +# --------------------------------------------------------------------------- +# WebApp teardown (used by destroy) +# --------------------------------------------------------------------------- + + +def destroy_webapp(state: dict) -> None: + """Run Terraform destroy to tear down WebApp infrastructure.""" + logger.info(" [dim]→ Running Terraform destroy for WebApp resources...[/dim]") + + tf_dir = Path(str(env.working_dir)).parent / "terraform-webapp" + + if not tf_dir.exists(): + logger.warning(f" [yellow]⚠[/yellow] [dim]Terraform directory not found at {tf_dir} skipping WebApp destroy[/dim]") + return + + # --- Check Terraform state, not Babylon state --- + tf_state_file = tf_dir / ".terraform" / "terraform.tfstate" + if not tf_state_file.exists(): + logger.warning(" [yellow]⚠[/yellow] [dim]No terraform state found, skipping WebApp destroy[/dim]") + return + + webapp_name = state.get("services", {}).get("webapp", {}).get("webapp_name") or "" + + try: + process = subprocess.Popen( + ["terraform", "destroy", "-auto-approve", "-lock=false", "-input=false"], + cwd=tf_dir, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + + line_handlers = { + "Destroy complete!": "green", + "Resources:": "green", + "Error": "red", + } + + for line in process.stdout: + clean_line = line.strip() + if not clean_line: + continue + color = next((color for key, color in line_handlers.items() if key in clean_line), "white") + bold = color == "red" + echo(style(f" {clean_line}", fg=color, bold=bold)) + + process.wait() + if process.returncode == 0: + state.setdefault("services", {}).setdefault("webapp", {}) + state["services"]["webapp"]["webapp_name"] = "" + state["services"]["webapp"]["webapp_url"] = "" + logger.info(f" [green]✔[/green] WebApp [magenta]{webapp_name}[/magenta] destroyed") + else: + logger.error(f" [bold red]✘[/bold red] Terraform destroy failed (Code {process.returncode})") + + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Error during WebApp destruction: {e}") diff --git a/Babylon/commands/macro/helpers/workspace.py b/Babylon/commands/macro/helpers/workspace.py new file mode 100644 index 000000000..88ad33b51 --- /dev/null +++ b/Babylon/commands/macro/helpers/workspace.py @@ -0,0 +1,527 @@ +""" +Helpers for workspace deployment and teardown, organised by concern: + + 1. Cosmotech API — create / update workspace + security sync + — generic API resource deletion (used by destroy) + 2. Kubernetes resources — Secret and ConfigMap lifecycle (create / delete) + 3. Kubernetes PostgreSQL — service discovery + schema init-job orchestration + — schema teardown (used by destroy) +""" + +import subprocess +from base64 import b64encode +from logging import getLogger +from pathlib import Path +from string import Template +from textwrap import dedent +from typing import Callable + +from cosmotech_api.models.workspace_create_request import WorkspaceCreateRequest +from cosmotech_api.models.workspace_security import WorkspaceSecurity +from cosmotech_api.models.workspace_update_request import WorkspaceUpdateRequest +from kubernetes import client, config, utils +from kubernetes import config as kube_config +from kubernetes.utils import FailToCreateError +from yaml import safe_load + +from Babylon.commands.macro.helpers.common import update_object_security +from Babylon.utils.environment import Environment + +logger = getLogger(__name__) +env = Environment() + + +# --------------------------------------------------------------------------- +# Cosmotech API helpers +# --------------------------------------------------------------------------- + + +def create_workspace(api_instance, api_section: dict, payload: dict, state: dict) -> bool: + """Create a new workspace and persist its ID in state. Returns False on failure.""" + logger.info(" [dim]→ No existing workspace ID found. Creating...[/dim]") + workspace = api_instance.create_workspace( + organization_id=api_section["organization_id"], + workspace_create_request=WorkspaceCreateRequest.from_dict(payload), + ) + if workspace is None: + logger.error(" [bold red]✘[/bold red] Failed to create workspace") + return False + logger.info(f" [bold green]✔[/bold green] Workspace [bold magenta]{workspace.id}[/bold magenta] created") + state["services"]["api"]["workspace_id"] = workspace.id + return True + + +def sync_workspace_security(api_instance, api_section: dict, payload: dict) -> bool: + """Synchronise security roles if a security block is present in the payload.""" + if not payload.get("security"): + return True + try: + logger.info(" [dim]→ Syncing security policies...[/dim]") + current_security = api_instance.get_workspace_security( + organization_id=api_section["organization_id"], workspace_id=api_section["workspace_id"] + ) + update_object_security( + "workspace", + current_security=current_security, + desired_security=WorkspaceSecurity.from_dict(payload.get("security")), + api_instance=api_instance, + object_id=[api_section["organization_id"], api_section["workspace_id"]], + ) + return True + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Security update failed: {e}") + return False + + +def update_workspace(api_instance, api_section: dict, payload: dict) -> bool: + """Update an existing workspace and sync its security policy. Returns False on failure.""" + logger.info(f" [dim]→ Existing ID [bold cyan]{api_section['workspace_id']}[/bold cyan] found. Updating...[/dim]") + updated = api_instance.update_workspace( + organization_id=api_section["organization_id"], + workspace_id=api_section["workspace_id"], + workspace_update_request=WorkspaceUpdateRequest.from_dict(payload), + ) + if updated is None: + logger.error(f" [bold red]✘[/bold red] Failed to update workspace {api_section['workspace_id']}") + return False + if not sync_workspace_security(api_instance, api_section, payload): + return False + logger.info(f" [bold green]✔[/bold green] Workspace [bold magenta]{api_section['workspace_id']}[/bold magenta] updated") + return True + + +def delete_api_resource( + api_call: Callable[..., None], resource_name: str, org_id: str | None, resource_id: str, state: dict, state_key: str +) -> None: + """Delete a Cosmotech API resource and clear its ID from state. + + Handles the repetitive deletion pattern shared across organization, solution + and workspace teardown. A 404 response is treated as a no-op (already gone). + """ + if not resource_id: + logger.warning(f" [yellow]⚠[/yellow] [dim]No {resource_name} ID found in state! skipping deletion[dim]") + return + + try: + logger.info(f" [dim]→ Existing ID [bold cyan]{resource_id}[/bold cyan] found. Deleting...[/dim]") + if org_id and resource_name != "Organization": + api_call(organization_id=org_id, **{f"{resource_name.lower()}_id": resource_id}) + else: + api_call(organization_id=resource_id) + + logger.info(f" [bold green]✔[/bold green] {resource_name} [magenta]{resource_id}[/magenta] deleted") + state["services"]["api"][state_key] = "" + except Exception as e: + error_msg = str(e) + if "404" in error_msg or "Not Found" in error_msg: + logger.info(f" [bold yellow]⚠[/bold yellow] {resource_name} [magenta]{resource_id}[/magenta] already deleted (404)") + state["services"]["api"][state_key] = "" + else: + logger.error(f" [bold red]✘[/bold red] Error deleting {resource_name.lower()} {resource_id} reason: {e}") + + +# --------------------------------------------------------------------------- +# Kubernetes Secret and ConfigMap lifecycle +# --------------------------------------------------------------------------- + + +def create_workspace_secret( + namespace: str, + organization_id: str, + workspace_id: str, + writer_password: str, +) -> bool: + """Create a Kubernetes Secret for a workspace containing API and PostgreSQL credentials. + + The secret is named ``-`` and holds all + environment variables required by workspace. + + Returns: + bool: True if the secret was created or already exists, False on error. + """ + secret_name = f"{organization_id}-{workspace_id}" + data = { + "POSTGRES_USER_PASSWORD": writer_password, + } + encoded_data = {k: b64encode(v.encode("utf-8")).decode("utf-8") for k, v in data.items()} + + secret = client.V1Secret( + api_version="v1", + kind="Secret", + metadata=client.V1ObjectMeta(name=secret_name, namespace=namespace), + type="Opaque", + data=encoded_data, + ) + + try: + config.load_kube_config() + v1 = client.CoreV1Api() + v1.create_namespaced_secret(namespace=namespace, body=secret) + logger.info(f" [bold green]✔[/bold green] Secret [magenta]{secret_name}[/magenta] created") + return True + except client.exceptions.ApiException as e: + if getattr(e, "status", None) == 409: + logger.warning(f" [yellow]⚠[/yellow] [dim]Secret [magenta]{secret_name}[/magenta] already exists[/dim]") + return True + logger.error(f" [bold red]✘[/bold red] Failed to create secret {secret_name}: {e.reason}") + return False + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Unexpected error creating secret {secret_name}") + logger.debug(f" Detail: {e}", exc_info=True) + return False + + +def create_coal_configmap( + namespace: str, + organization_id: str, + workspace_id: str, + db_host: str, + db_port: str, + db_name: str, + schema_name: str, + writer_username: str, +) -> bool: + """Create a CoAL ConfigMap for a workspace. + + The ConfigMap is named ``--coal-config`` and + contains a ``coal-config.toml`` key with Postgres output configuration. The + ``user_password`` value is deliberately set to the literal string + ``env.POSTGRES_USER_PASSWORD`` so that the CoAL runtime resolves it from the + environment at execution time. + + Returns: + bool: True if the ConfigMap was created or already exists, False on error. + """ + configmap_name = f"{organization_id}-{workspace_id}-coal-config" + coal_toml = dedent(f"""\ + [[outputs]] + type = "postgres" + [outputs.conf.postgres] + host = "{db_host}" + port = "{db_port}" + db_name = "{db_name}" + db_schema = "{schema_name}" + user_name = "{writer_username}" + user_password = "env.POSTGRES_USER_PASSWORD" + """) + + configmap = client.V1ConfigMap( + api_version="v1", + kind="ConfigMap", + metadata=client.V1ObjectMeta(name=configmap_name, namespace=namespace), + data={"coal-config.toml": coal_toml}, + ) + + try: + config.load_kube_config() + v1 = client.CoreV1Api() + v1.create_namespaced_config_map(namespace=namespace, body=configmap) + logger.info(f" [bold green]✔[/bold green] ConfigMap [magenta]{configmap_name}[/magenta] created") + return True + except client.ApiException as e: + if e.status == 409: + logger.warning(f" [yellow]⚠[/yellow] [dim]ConfigMap [magenta]{configmap_name}[/magenta] already exists[/dim]") + return True + logger.error(f" [bold red]✘[/bold red] Failed to create ConfigMap {configmap_name}: {e.reason}") + return False + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Unexpected error creating ConfigMap {configmap_name}") + logger.debug(f" Detail: {e}", exc_info=True) + return False + + +def delete_kubernetes_resources(namespace: str, organization_id: str, workspace_id: str) -> None: + """Delete the Workspace Secret and CoAL ConfigMap created during deployment. + + Targets: + - Secret: ``-`` + - ConfigMap: ``--coal-config`` + + If a resource is already gone (404), a warning is logged and execution + continues without error. + """ + secret_name = f"{organization_id}-{workspace_id}" + configmap_name = f"{organization_id}-{workspace_id}-coal-config" + + try: + config.load_kube_config() + v1 = client.CoreV1Api() + except Exception as e: + logger.error(" [bold red]✘[/bold red] Failed to initialise Kubernetes client") + logger.debug(f" Detail: {e}", exc_info=True) + return + + # --- Delete Secret --- + try: + logger.info(" [dim]→ Deleting workspace Secret ...[/dim]") + v1.delete_namespaced_secret(name=secret_name, namespace=namespace) + logger.info(f" [bold green]✔[/bold green] Secret [magenta]{secret_name}[/magenta] deleted") + except client.ApiException as e: + if e.status == 404: + logger.warning(" [yellow]⚠[/yellow] [dim]Secret not found (already deleted)[/dim]") + else: + logger.error(f" [bold red]✘[/bold red] Failed to delete secret {secret_name}: {e.reason}") + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Unexpected error deleting secret {secret_name}") + logger.debug(f" Detail: {e}", exc_info=True) + + # --- Delete ConfigMap --- + try: + logger.info(" [dim]→ Deleting workspace ConfigMap ...[/dim]") + v1.delete_namespaced_config_map(name=configmap_name, namespace=namespace) + logger.info(f" [bold green]✔[/bold green] ConfigMap [magenta]{configmap_name}[/magenta] deleted") + except client.ApiException as e: + if e.status == 404: + logger.warning(" [yellow]⚠[/yellow] [dim]ConfigMap not found (already deleted)[/dim]") + else: + logger.error(f" [bold red]✘[/bold red] Failed to delete ConfigMap {configmap_name}: {e.reason}") + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Unexpected error deleting ConfigMap {configmap_name}") + logger.debug(f" Detail: {e}", exc_info=True) + + +# --------------------------------------------------------------------------- +# Kubernetes PostgreSQL service discovery +# --------------------------------------------------------------------------- + + +def get_postgres_service_host(namespace: str) -> str: + """Discover the PostgreSQL service name in a namespace to build its FQDN. + + Note: This function assumes PostgreSQL is running within the same Kubernetes cluster. + External database clusters are not currently supported. + """ + try: + config.load_kube_config() + v1 = client.CoreV1Api() + services = v1.list_namespaced_service(namespace) + + for svc in services.items: + labels = svc.metadata.labels or {} + if "postgresql" in svc.metadata.name or labels.get("app.kubernetes.io/name") == "postgresql": + logger.info(f" [dim]→ Found PostgreSQL service {svc.metadata.name}[/dim]") + return f"{svc.metadata.name}.{namespace}.svc.cluster.local" + + return f"postgresql.{namespace}.svc.cluster.local" + except Exception as e: + logger.warning(" [bold yellow]⚠[/bold yellow] Service discovery failed ! default will be used.") + logger.debug(f" Exception details: {e}", exc_info=True) + return f"postgresql.{namespace}.svc.cluster.local" + + +# --------------------------------------------------------------------------- +# Kubernetes PostgreSQL schema init-job orchestration +# --------------------------------------------------------------------------- + + +def _handle_init_job_logs(k8s_job_name: str, schema_name: str, state: dict) -> None: + """Fetch init-job logs and update state based on their content.""" + logs_process = subprocess.run( + ["kubectl", "logs", f"job/{k8s_job_name}", "-n", env.environ_id], + capture_output=True, + text=True, + ) + if logs_process.returncode != 0: + logger.error(f" [bold red]✘[/bold red] Failed to retrieve logs for job {k8s_job_name}") + logger.debug(f" [bold red]✘[/bold red] Logs retrieval output {logs_process.stdout} {logs_process.stderr}") + return + + job_logs = logs_process.stdout or logs_process.stderr + if "ERROR" in job_logs or "error" in job_logs: + logger.error(" [bold red]✘[/bold red] Schema creation failed inside the container") + logger.debug(f" [bold red]✘[/bold red] Job logs : {job_logs}") + elif "already exists" in job_logs: + logger.info(f" [yellow]⚠[/yellow] [dim]Schema [magenta]{schema_name}[/magenta] already exists (skipping creation)[/dim]") + else: + logger.info(f" [green]✔[/green] Schema creation [magenta]{schema_name}[/magenta] completed successfully") + state["services"]["postgres"]["schema_name"] = schema_name + + +def _wait_and_check_init_job(k8s_job_name: str, schema_name: str, state: dict) -> None: + """Wait for the init job to complete, then inspect its logs.""" + logger.info(f" [dim]→ Waiting for job [cyan]{k8s_job_name}[/cyan] to complete...[/dim]") + wait_process = subprocess.run( + ["kubectl", "wait", "--for=condition=complete", "job", k8s_job_name, f"--namespace={env.environ_id}", "--timeout=50s"], + capture_output=True, + text=True, + ) + if wait_process.returncode != 0: + logger.error(f" [bold red]✘[/bold red] Job {k8s_job_name} did not complete successfully see babylon logs for details") + logger.debug(f" [bold red]✘[/bold red] Job wait output {wait_process.stdout} {wait_process.stderr}") + return + logger.info(" [dim]→ Checking job logs for errors...[/dim]") + _handle_init_job_logs(k8s_job_name, schema_name, state) + + +def _run_schema_init_job(script_path: Path, mapping: dict, workspace_id: str, schema_name: str, state: dict) -> None: + """Apply a single K8s init job from *script_path* and wait for its outcome.""" + k8s_job_name = f"postgresql-init-{workspace_id}" + kube_config.load_kube_config() + k8s_client = client.ApiClient() + + with open(script_path, "r") as f: + raw_content = f.read() + + yaml_dict = safe_load(Template(raw_content).safe_substitute(mapping)) + try: + utils.create_from_dict(k8s_client, yaml_dict, namespace=env.environ_id) + _wait_and_check_init_job(k8s_job_name, schema_name, state) + except FailToCreateError as e: + for inner_exception in e.api_exceptions: + if inner_exception.status == 409: + logger.warning(f" [yellow]⚠[/yellow] [dim]Job [cyan]{k8s_job_name}[/cyan] already exists.[/dim]") + else: + logger.error(f" [bold red]✘[/bold red] K8s Error ({inner_exception.status}): {inner_exception.reason}") + logger.debug(f" Detail: {inner_exception.body}") + except Exception as e: + logger.error(" [bold red]✘[/bold red] Unexpected error please check babylon logs file for details") + logger.debug(f" [bold red]✘[/bold red] {e}") + + +def deploy_postgres_schema(workspace_id: str, schema_config: dict, api_section: dict, deploy_dir: Path, state: dict) -> None: + """Initialise the PostgreSQL schema and create the associated K8s resources.""" + db_host = get_postgres_service_host(env.environ_id) + logger.info(f" [dim]→ Initializing PostgreSQL schema for workspace [bold cyan]{workspace_id}[/bold cyan]...[/dim]") + + pg_config = env.get_config_from_k8s_secret_by_tenant("postgresql-config", env.environ_id) + api_config = env.get_config_from_k8s_secret_by_tenant("postgresql-cosmotechapi", env.environ_id) + if not pg_config or not api_config: + return + + schema_name = workspace_id.replace("-", "_") + mapping = { + "namespace": env.environ_id, + "db_host": db_host, + "db_port": "5432", + "cosmotech_api_database": api_config.get("database-name", ""), + "cosmotech_api_admin_username": api_config.get("admin-username", ""), + "cosmotech_api_admin_password": api_config.get("admin-password", ""), + "cosmotech_api_writer_username": api_config.get("writer-username", ""), + "cosmotech_api_reader_username": api_config.get("reader-username", ""), + "workspace_schema": schema_name, + "job_name": workspace_id, + } + + deploy_dir = deploy_dir if isinstance(deploy_dir, Path) else Path(deploy_dir) + for job in schema_config.get("jobs", []): + script_path = deploy_dir / job.get("path", "") / job.get("name", "") + if script_path.exists(): + _run_schema_init_job(script_path, mapping, workspace_id, schema_name, state) + + organization_id = api_section["organization_id"] + logger.info(f" [dim]→ Creating workspace secret for [cyan]{workspace_id}[/cyan]...[/dim]") + create_workspace_secret( + namespace=env.environ_id, + organization_id=organization_id, + workspace_id=workspace_id, + writer_password=api_config.get("writer-password", ""), + ) + logger.info(f" [dim]→ Creating CoAL ConfigMap for [cyan]{workspace_id}[/cyan]...[/dim]") + create_coal_configmap( + namespace=env.environ_id, + organization_id=organization_id, + workspace_id=workspace_id, + db_host=db_host, + db_port="5432", + db_name=api_config.get("database-name", ""), + schema_name=schema_name, + writer_username=api_config.get("writer-username", ""), + ) + + +# --------------------------------------------------------------------------- +# Kubernetes PostgreSQL schema teardown (used by destroy) +# --------------------------------------------------------------------------- + + +def _handle_destroy_job_logs(k8s_job_name: str, schema_name: str, state: dict) -> None: + """Fetch destroy-job logs and update state based on their content.""" + logs_process = subprocess.run( + ["kubectl", "logs", f"job/{k8s_job_name}", "-n", env.environ_id], + capture_output=True, + text=True, + ) + if logs_process.returncode != 0: + logger.error(f" [bold red]✘[/bold red] Failed to retrieve logs for job {k8s_job_name}") + logger.debug(f" [bold red]✘[/bold red] Logs retrieval output {logs_process.stdout} {logs_process.stderr}") + return + + job_logs = logs_process.stdout or logs_process.stderr + if "ERROR" in job_logs or "error" in job_logs: + logger.error(" [bold red]✘[/bold red] Schema destruction failed inside the container") + logger.debug(f" [bold red]✘[/bold red] Job logs : {job_logs}") + elif "does not exist" in job_logs: + logger.info(f" [yellow]⚠[/yellow] [dim]Schema [magenta]{schema_name}[/magenta] does not exist (nothing to clean)[/dim]") + state["services"]["postgres"]["schema_name"] = "" + else: + logger.info(f" [green]✔[/green] Schema destruction [magenta]{schema_name}[/magenta] completed successfully") + state["services"]["postgres"]["schema_name"] = "" + + +def _wait_and_check_destroy_job(k8s_job_name: str, schema_name: str, state: dict) -> None: + """Wait for the destroy job to complete, then inspect its logs.""" + logger.info(f" [dim]→ Waiting for job [cyan]{k8s_job_name}[/cyan] to complete...[/dim]") + wait_process = subprocess.run( + ["kubectl", "wait", "--for=condition=complete", "job", k8s_job_name, f"--namespace={env.environ_id}", "--timeout=300s"], + capture_output=True, + text=True, + ) + if wait_process.returncode != 0: + logger.error(f" [bold red]✘[/bold red] Job {k8s_job_name} did not complete successfully see babylon logs for details") + logger.debug(f" [bold red]✘[/bold red] Job wait output {wait_process.stdout} {wait_process.stderr}") + return + + logger.info(" [dim]→ Checking job logs for errors...[/dim]") + _handle_destroy_job_logs(k8s_job_name, schema_name, state) + + +def destroy_postgres_schema(schema_name: str, state: dict) -> None: + """Destroy the PostgreSQL schema for a workspace. + + Applies a K8s destroy job rendered from the template at + ``env.original_template_path / yaml / k8s_job_destroy.yaml``, waits for + it to complete and clears the schema name from state on success. + """ + if not schema_name: + logger.warning(" [yellow]⚠[/yellow] [dim]No schema found ! skipping deletion[/dim]") + return + + workspace_id_tmp = schema_name.replace("_", "-") + db_host = get_postgres_service_host(env.environ_id) + logger.info(f" [dim]→ Destroying postgreSQL schema for workspace [bold cyan]{workspace_id_tmp}[/bold cyan]...[/dim]") + + pg_config = env.get_config_from_k8s_secret_by_tenant("postgresql-config", env.environ_id) + api_config = env.get_config_from_k8s_secret_by_tenant("postgresql-cosmotechapi", env.environ_id) + + if not pg_config or not api_config: + logger.error(" [bold red]✘[/bold red] Failed to retrieve postgreSQL configuration from secrets") + return + + mapping = { + "namespace": env.environ_id, + "db_host": db_host, + "db_port": "5432", + "cosmotech_api_database": api_config.get("database-name"), + "cosmotech_api_admin_username": api_config.get("admin-username"), + "cosmotech_api_admin_password": api_config.get("admin-password"), + "cosmotech_api_writer_username": api_config.get("writer-username"), + "cosmotech_api_reader_username": api_config.get("reader-username"), + "workspace_schema": schema_name, + "job_name": workspace_id_tmp, + } + destroy_jobs = env.original_template_path / "yaml" / "k8s_job_destroy.yaml" + k8s_job_name = f"postgresql-destroy-{workspace_id_tmp}" + kube_config.load_kube_config() + k8s_client = client.ApiClient() + + with open(destroy_jobs, "r") as f: + raw_content = f.read() + + yaml_dict = safe_load(Template(raw_content).safe_substitute(mapping)) + logger.info(" [dim]→ Applying kubernetes destroy job...[/dim]") + try: + utils.create_from_dict(k8s_client, yaml_dict, namespace=env.environ_id) + _wait_and_check_destroy_job(k8s_job_name, schema_name, state) + except Exception as e: + logger.error(" [bold red]✘[/bold red] Unexpected error please check babylon logs file for details") + logger.debug(f" [bold red]✘[/bold red] {e}") diff --git a/Babylon/commands/macro/init.py b/Babylon/commands/macro/init.py index 62a29de48..0f8560d56 100644 --- a/Babylon/commands/macro/init.py +++ b/Babylon/commands/macro/init.py @@ -1,75 +1,177 @@ import subprocess from logging import getLogger -from os import getcwd from pathlib import Path from shutil import copy -from click import command, echo, option, style +from click import Choice, argument, command, echo, option, style from Babylon.utils.environment import Environment logger = getLogger(__name__) env = Environment() +# Constants + +_TF_WEBAPP_DIR = "terraform-webapp" +_TF_WEBAPP_REPO_URL = "https://github.com/Cosmo-Tech/terraform-webapp.git" +_VARIABLES_TEMPLATE = "variables.yaml" + +_PROJECT_YAML_FILES = [ + "Organization.yaml", + "Solution.yaml", + "Workspace.yaml", +] + +# Cloud providers that have their own yaml sub-directory +_SUPPORTED_CLOUD_PROVIDERS = {"azure", "kob"} + +# Private helpers + + +def _get_provider_template(cloud_provider: str, filename: str) -> Path: + """Return the template path for *filename* scoped to *cloud_provider* when available, + falling back to the shared yaml directory otherwise.""" + provider = cloud_provider.lower() + if provider in _SUPPORTED_CLOUD_PROVIDERS: + return env.original_template_path / "yaml" / provider / filename + return env.original_template_path / "yaml" / filename + + +def _clone_webapp(tf_webapp_path: Path) -> None: + """Clone the Terraform WebApp repository into *tf_webapp_path*.""" + logger.info(" [dim]→ Cloning Terraform WebApp module...[/dim]") + try: + subprocess.run( + ["git", "clone", "-q", _TF_WEBAPP_REPO_URL, str(tf_webapp_path)], + check=True, + stdout=subprocess.DEVNULL, + ) + if tf_webapp_path.exists(): + logger.info(" [green]✔[/green] Terraform WebApp module cloned") + else: + logger.error(" [bold red]✘[/bold red] Terraform WebApp module was not created after cloning") + except subprocess.CalledProcessError as exc: + logger.error(f" [bold red]✘[/bold red] Failed to clone Terraform repo: {exc}") + + +def _ensure_webapp(tf_webapp_path: Path) -> None: + """Log success when *tf_webapp_path* exists, otherwise clone it.""" + if tf_webapp_path.exists(): + logger.info(" [green]✔[/green] Webapp directory [cyan]terraform-webapp[/cyan] already exists.") + else: + logger.warning(" [bold yellow]![/bold yellow] Webapp directory not found") + _clone_webapp(tf_webapp_path) + + +def _ensure_variables_file(variables_path: Path, variables_file: str, cloud_provider: str) -> None: + """Log success when *variables_path* exists, otherwise copy the template.""" + if variables_path.exists(): + logger.info(f" [green]✔[/green] Variables file [cyan]{variables_file}[/cyan] already exists.") + return + + logger.warning(" [bold yellow]![/bold yellow] Variables file not found") + logger.info(" [dim]→ Generating variables file from template...[/dim]") + try: + variables_template = _get_provider_template(cloud_provider, _VARIABLES_TEMPLATE) + copy(variables_template, variables_path) + if variables_path.exists(): + logger.info(f" [green]✔[/green] Generated [cyan]{variables_file}[/cyan]") + else: + logger.error(f" [bold red]✘[/bold red] Failed to generate [cyan]{variables_file}[/cyan]") + except OSError as exc: + logger.error(f" [bold red]✘[/bold red] Failed to generate variables file: {exc}") + + +def _scaffold_project( + project_path: Path, variables_path: Path, variables_file: str, tf_webapp_path: Path, cloud_provider: str +) -> None: + """Create the full project directory structure and copy all template files.""" + try: + _create_project_dir(project_path) + _copy_yaml_templates(project_path, cloud_provider) + _create_postgres_jobs(project_path) + _copy_variables_template(variables_path, variables_file, cloud_provider) + _ensure_webapp(tf_webapp_path) + _print_success_summary(project_path, variables_file) + except OSError as exc: + logger.error(" [bold red]✘[/bold red] An error occurred while scaffolding see babylon logs for details") + logger.debug(f" [bold red]✘[/bold red] Error details: {exc}", exc_info=True) + + +def _create_project_dir(project_path: Path) -> None: + project_path.mkdir(parents=True, exist_ok=True) + if project_path.exists(): + logger.info(f" [dim]→ Created directory: {project_path}[/dim]") + else: + logger.error(f" [bold red]✘[/bold red] Failed to create directory: {project_path}") + + +def _copy_yaml_templates(project_path: Path, cloud_provider: str) -> None: + for filename in _PROJECT_YAML_FILES: + src = env.original_template_path / "yaml" / filename + copy(src, project_path / filename) + logger.info(f" [green]✔[/green] Generated [white]{filename}[/white]") + + # Copy the cloud-provider-specific Webapp.yaml + webapp_src = _get_provider_template(cloud_provider, "Webapp.yaml") + copy(webapp_src, project_path / "Webapp.yaml") + logger.info(f" [green]✔[/green] Generated [white]Webapp.yaml[/white] (provider: {cloud_provider})") + + +def _create_postgres_jobs(project_path: Path) -> None: + postgres_jobs_path = project_path / "postgres" / "jobs" + postgres_jobs_path.mkdir(parents=True, exist_ok=True) + if postgres_jobs_path.exists(): + logger.info(" [dim]→ Created directory: postgres/jobs[/dim]") + else: + logger.error(" [bold red]✘[/bold red] Failed to create directory: postgres/jobs") + + k8s_template = env.original_template_path / "yaml" / "k8s_job.yaml" + if k8s_template.exists(): + copy(k8s_template, postgres_jobs_path / "k8s_job.yaml") + logger.info(" [green]✔[/green] Generated [white]postgres/jobs/k8s_job.yaml[/white]") + + +def _copy_variables_template(variables_path: Path, variables_file: str, cloud_provider: str) -> None: + variables_template = _get_provider_template(cloud_provider, _VARIABLES_TEMPLATE) + copy(variables_template, variables_path) + if variables_path.exists(): + logger.info(f" [green]✔[/green] Generated [white]{variables_file}[/white] (provider: {cloud_provider})") + else: + logger.error(f" [bold red]✘[/bold red] Failed to generate [white]{variables_file}[/white]") + + +def _print_success_summary(project_path: Path, variables_file: str) -> None: + echo(style("\n🚀 Project successfully initialized!", fg="green", bold=True)) + echo(style(f" Path: {project_path}", fg="white", dim=True)) + echo(style("\nNext steps:", fg="white", bold=True)) + echo(style(f" 1. Edit your variables in {variables_file}", fg="cyan")) + echo(style(" 2. Run your first deployment command", fg="cyan")) + @command() @option("--project-folder", default="project", help="Name of the project folder to create (default: 'project').") @option("--variables-file", default="variables.yaml", help="Name of the variables file (default: 'variables.yaml').") -def init(project_folder: str, variables_file: str): +@argument("cloud_provider", type=Choice(["azure", "kob"], case_sensitive=False)) +def init(project_folder: str, variables_file: str, cloud_provider: str): """ Scaffolds a new Babylon project structure using YAML templates. + + arguments: + + cloud_provider: Target cloud provider for webapp deployment (e.g. 'azure', 'kob'). """ - project_path = Path(getcwd()) / project_folder - variables_path = Path(getcwd()) / variables_file + cwd = Path.cwd() + project_path = cwd / project_folder + variables_path = cwd / variables_file + tf_webapp_path = cwd / _TF_WEBAPP_DIR + + # Validation mode: project folder already exists — check each component. if project_path.exists(): - logger.warning(f"The directory [bold]{project_path}[/bold] already exists") - return None - if variables_path.exists(): - logger.warning(f"Configuration file [bold]{variables_file}[/bold] already exists.") + logger.info(f" [green]✔[/green] Project directory [cyan]{project_folder}[/cyan] already exists.") + _ensure_webapp(tf_webapp_path) + _ensure_variables_file(variables_path, variables_file, cloud_provider) return None - tf_webapp_path = Path(getcwd()) / "terraform-webapp" - repo_url = "https://github.com/Cosmo-Tech/terraform-webapp.git" - if not tf_webapp_path.exists(): - logger.info(" [dim]→ Cloning Terraform WebApp module...[/dim]") - try: - subprocess.run(["git", "clone", "-q", repo_url, str(tf_webapp_path)], check=True, stdout=subprocess.DEVNULL) - logger.info(" [green]✔[/green] Terraform WebApp module cloned") - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to clone Terraform repo: {e}") - project_yaml_files = [ - "Organization.yaml", - "Solution.yaml", - "Workspace.yaml", - "Webapp.yaml", - ] - try: - # Create project directory - project_path.mkdir(parents=True, exist_ok=True) - logger.info(f" [dim]→ Created directory: {project_path}[/dim]") - # Copy Core YAML Templates - for file in project_yaml_files: - deploy_file = env.original_template_path / "yaml" / file - destination = project_path / file - copy(deploy_file, destination) - logger.info(f" [green]✔[/green] Generated [white]{file}[/white]") - - postgres_jobs_path = project_path / "postgres" / "jobs" - postgres_jobs_path.mkdir(parents=True, exist_ok=True) - k8s_template = env.original_template_path / "yaml" / "k8s_job.yaml" - if k8s_template.exists(): - copy(k8s_template, postgres_jobs_path / "k8s_job.yaml") - logger.info(" [green]✔[/green] Generated [white]postgres/jobs/k8s_job.yaml[/white]") - - variables_template = env.original_template_path / "yaml" / "variables.yaml" - copy(variables_template, variables_path) - logger.info(f" [green]✔[/green] Generated [white]{variables_file}[/white]") - - # --- 3. Success Summary --- - echo(style("\n🚀 Project successfully initialized!", fg="green", bold=True)) - echo(style(f" Path: {project_path}", fg="white", dim=True)) - echo(style("\nNext steps:", fg="white", bold=True)) - echo(style(f" 1. Edit your variables in {variables_file}", fg="cyan")) - echo(style(" 2. Run your first deployment command", fg="cyan")) - except Exception as e: - logger.error(" [bold red]✘[/bold red] An error occurred while scaffolding see babylon logs for details") - logger.debug(f" [bold red]✘[/bold red] Error details: {e}", exc_info=True) + + # Scaffold mode: nothing exists yet — build everything from scratch. + _scaffold_project(project_path, variables_path, variables_file, tf_webapp_path, cloud_provider) diff --git a/Babylon/commands/namespace/get_all_states.py b/Babylon/commands/namespace/get_all_states.py index 2c12029aa..0d6389377 100644 --- a/Babylon/commands/namespace/get_all_states.py +++ b/Babylon/commands/namespace/get_all_states.py @@ -9,38 +9,39 @@ env = Environment() +def _get_local_states() -> bool: + echo(style("\n 📂 Local States", bold=True, fg="cyan")) + if not env.state_dir.exists(): + logger.error(f" [bold red]✘[/bold red] Directory not found: [dim]{env.state_dir}[/dim]") + return False + local_files = sorted(env.state_dir.glob("state.*.yaml")) + if not local_files: + logger.warning(" [yellow]⚠[/yellow] No local state files found") + return False + for f in local_files: + echo(style(" • ", fg="green") + f.name) + return True + + +def _get_remote_states() -> bool: + echo(style("\n ☁️ Remote States", bold=True, fg="cyan")) + try: + remote_files = env.list_remote_states() + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Failed to reach remote storage: {e}") + return False + if not remote_files: + logger.warning(" [yellow]⚠[/yellow] No remote states found") + return False + for name in sorted(remote_files): + echo(style(" • ", fg="green") + name) + return True + + @command() @argument("target", type=Choice(["local", "remote"], case_sensitive=False)) def get_states(target: str) -> CommandResponse: - """Display states from local machine or Azure remote storage.""" - - results_found = False - if target == "local": - echo(style("\n 📂 Local States", bold=True, fg="cyan")) - states_dir = env.state_dir - - if not states_dir.exists(): - logger.error(f" [bold red]✘[/bold red] Directory not found: [dim]{states_dir}[/dim]") - else: - local_files = sorted(states_dir.glob("state.*.yaml")) - if not local_files: - logger.warning(" [yellow]⚠[/yellow] No local state files found") - else: - for f in local_files: - echo(style(" • ", fg="green") + f.name) - results_found = True - - elif target == "remote": - echo(style("\n ☁️ Remote States", bold=True, fg="cyan")) - try: - remote_files = env.list_remote_states() - if not remote_files: - logger.warning(" [yellow]⚠[/yellow] No remote states found on Azure") - else: - for name in sorted(remote_files): - echo(style(" • ", fg="green") + name) - results_found = True - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to reach Azure storage: {e}") - + """Display states from local machine or remote Kubernetes storage.""" + handlers = {"local": _get_local_states, "remote": _get_remote_states} + results_found = handlers[target]() return CommandResponse.success() if results_found else CommandResponse.fail() diff --git a/Babylon/commands/namespace/get_contexts.py b/Babylon/commands/namespace/get_contexts.py index d72a69476..6cb7ac1d9 100644 --- a/Babylon/commands/namespace/get_contexts.py +++ b/Babylon/commands/namespace/get_contexts.py @@ -13,8 +13,8 @@ def get_contexts() -> CommandResponse: """Display the currently active namespace""" namespace = env.get_namespace_from_local() - headers = ["CURRENT", "CONTEXT", "TENANT", "STATE ID"] - values = ["*", namespace.get("context", ""), namespace.get("tenant", ""), namespace.get("state_id", "")] + headers = ["CURRENT", "CONTEXT", "TENANT"] + values = ["*", namespace.get("context", ""), namespace.get("tenant", "")] col_widths = [max(len(h), len(v)) + 2 for h, v in zip(headers, values)] header_line = "".join(h.ljust(w) for h, w in zip(headers, col_widths)) value_line = "".join(v.ljust(w) for v, w in zip(values, col_widths)) diff --git a/Babylon/templates/working_dir/.templates/webapp/app_insight.json b/Babylon/templates/working_dir/.templates/webapp/app_insight.json deleted file mode 100644 index c3b928fa8..000000000 --- a/Babylon/templates/working_dir/.templates/webapp/app_insight.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "web", - "location": "${cosmotech['webapp']['location']}", - "properties": { - "Application_Type": "web", - "DisableIpMasking": false, - "Flow_Type": "Bluefield", - "HockeyAppId": "", - "ImmediatePurgeDataOn30Days": false, - "IngestionMode": "ApplicationInsights", - "Request_Source": "rest", - "RetentionInDays": 90, - "SamplingPercentage": 100 - }, - "tags": "" -} diff --git a/Babylon/templates/working_dir/.templates/webapp/webapp_config.yaml b/Babylon/templates/working_dir/.templates/webapp/webapp_config.yaml deleted file mode 100644 index a02b03c80..000000000 --- a/Babylon/templates/working_dir/.templates/webapp/webapp_config.yaml +++ /dev/null @@ -1,12 +0,0 @@ -REACT_APP_APPLICATION_INSIGHTS_INSTRUMENTATION_KEY: "${cosmotech['webapp']['insights_instrumentation_key']}" -REACT_APP_ENABLE_APPLICATION_INSIGHTS: "${cosmotech['webapp']['enable_insights']}" -REACT_APP_APP_REGISTRATION_CLIENT_ID: "${cosmotech['app']['app_id']}" -REACT_APP_AZURE_TENANT_ID: "${cosmotech['azure']['tenant_id']}" -REACT_APP_COSMOTECH_API_SCOPE: "${cosmotech['api']['scope']}" -REACT_APP_DEFAULT_BASE_PATH: "${cosmotech['api']['url']}" -REACT_APP_ORGANIZATION_ID: "${cosmotech['api']['organization_id']}" -REACT_APP_WORKSPACES_IDS_FILTER: '' -REACT_APP_APP_VERSION: '' -REACT_APP_ORGANIZATION_URL: "${cosmotech['api']['organization_url']}" -REACT_APP_DOCUMENTATION_URL: https://cosmotech.com -REACT_APP_SUPPORT_URL: https://support.cosmotech.com diff --git a/Babylon/templates/working_dir/.templates/webapp/webapp_details.yaml b/Babylon/templates/working_dir/.templates/webapp/webapp_details.yaml deleted file mode 100644 index d424efac5..000000000 --- a/Babylon/templates/working_dir/.templates/webapp/webapp_details.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: "{{webapp_name}}" -location: "{{webapp_location}}" -properties: - repositoryUrl: "{{webapp_repository}}" - branch: "{{webapp_branch}}" - repositoryToken: "{{github_secret}}" - buildProperties: - appLocation: "/" - apiLocation: api - appArtifactLocation: build -sku: - name: Standard - tier: Standard \ No newline at end of file diff --git a/Babylon/templates/working_dir/.templates/yaml/Organization.yaml b/Babylon/templates/working_dir/.templates/yaml/Organization.yaml index 6f85ff5c9..9505cd0c7 100644 --- a/Babylon/templates/working_dir/.templates/yaml/Organization.yaml +++ b/Babylon/templates/working_dir/.templates/yaml/Organization.yaml @@ -1,6 +1,6 @@ kind: Organization namespace: - remote: false + remote: {{remote}} spec: sidecars: payload: diff --git a/Babylon/templates/working_dir/.templates/yaml/Solution.yaml b/Babylon/templates/working_dir/.templates/yaml/Solution.yaml index 476e07ab4..022e6529b 100644 --- a/Babylon/templates/working_dir/.templates/yaml/Solution.yaml +++ b/Babylon/templates/working_dir/.templates/yaml/Solution.yaml @@ -1,6 +1,6 @@ kind: Solution namespace: - remote: false + remote: {{remote}} metadata: selector: organization_id: "{{services['api.organization_id']}}" diff --git a/Babylon/templates/working_dir/.templates/yaml/Workspace.yaml b/Babylon/templates/working_dir/.templates/yaml/Workspace.yaml index b791fd67f..1cf1ae4d9 100644 --- a/Babylon/templates/working_dir/.templates/yaml/Workspace.yaml +++ b/Babylon/templates/working_dir/.templates/yaml/Workspace.yaml @@ -1,6 +1,6 @@ kind: Workspace namespace: - remote: false + remote: {{remote}} spec: sidecars: postgres: diff --git a/Babylon/templates/working_dir/.templates/yaml/Webapp.yaml b/Babylon/templates/working_dir/.templates/yaml/azure/Webapp.yaml similarity index 76% rename from Babylon/templates/working_dir/.templates/yaml/Webapp.yaml rename to Babylon/templates/working_dir/.templates/yaml/azure/Webapp.yaml index 8ca27c4ab..c8f541048 100644 --- a/Babylon/templates/working_dir/.templates/yaml/Webapp.yaml +++ b/Babylon/templates/working_dir/.templates/yaml/azure/Webapp.yaml @@ -1,9 +1,9 @@ kind: Webapp namespace: - remote: false + remote: {{remote}} spec: payload: - cloud_provider: "azure" + cloud_provider: "{{cloud_provider}}" cluster_name: "{{cluster_name}}" domain_zone: "{{domain_zone}}" tenant: "{{tenant}}" @@ -11,4 +11,4 @@ spec: organization_id: "{{services['api.organization_id']}}" azure_subscription_id: "{{azure_subscription_id}}" azure_entra_tenant_id: "{{azure_entra_tenant_id}}" - powerbi_app_deploy: false \ No newline at end of file + powerbi_app_deploy: {{powerbi_app_deploy}} \ No newline at end of file diff --git a/Babylon/templates/working_dir/.templates/yaml/variables.yaml b/Babylon/templates/working_dir/.templates/yaml/azure/variables.yaml similarity index 74% rename from Babylon/templates/working_dir/.templates/yaml/variables.yaml rename to Babylon/templates/working_dir/.templates/yaml/azure/variables.yaml index d94b2f29b..358b8872d 100644 --- a/Babylon/templates/working_dir/.templates/yaml/variables.yaml +++ b/Babylon/templates/working_dir/.templates/yaml/azure/variables.yaml @@ -15,8 +15,14 @@ workspace_name: Babylon v5 workspace workspace_key: brewerytestingwork workspace_description: Testing workspace for the brewery web application # Webapp +# These variables are used to render your Webapp.yaml manifest and can be mapped to the +# terraform module variables (terraform.tfvars) used by the terraform-webapp module. +# See the module's example tfvars for reference: +# https://github.com/Cosmo-Tech/terraform-webapp/blob/main/terraform.tfvars + +## VARIABLES EXAMPLE FOR AZURE cloud_provider: azure -cluster_name: aks-dev-luxor +cluster_name: aks-dev-joy domain_zone: azure.platform.cosmotech.com tenant: sphinx webapp_name: business @@ -24,6 +30,10 @@ organization_id: o-xxxxxxxxxxx azure_subscription_id: a24b131f-bd0b-42e8-872a-bded9b91ab74 azure_entra_tenant_id: e413b834-8be8-4822-a370-be619545cb49 powerbi_app_deploy: false + +# Remote state +remote: true + # ACL security: default: none diff --git a/Babylon/templates/working_dir/.templates/yaml/dataset/customers.csv b/Babylon/templates/working_dir/.templates/yaml/dataset/customers.csv deleted file mode 100644 index 5b2e65d0e..000000000 --- a/Babylon/templates/working_dir/.templates/yaml/dataset/customers.csv +++ /dev/null @@ -1,11 +0,0 @@ -customerID,companyName,contactName,contactTitle,address,city,region,postalCode,country,phone,age -CUST001,Blueberry Tech,Alex Greenwood,CTO,1412 Pine St,Oakridge,CA,93561,USA,555-123-4560,10 -CUST002,Riverbend Logistics,Taylor Morris,Operations Lead,89 North Ave,Greendale,NY,11105,USA,555-987-6521,25 -CUST003,Sunrise Foods,Jordan Liu,Procurement Manager,220 Cherry Road,Springdale,TX,75010,USA,555-210-7895,34 -CUST004,Skyline Textiles,Maria Petrova,Sales Director,501 Textile Lane,Newpark,FL,33012,USA,555-313-9240,59 -CUST005,Crimson Solutions,Samuel Evans,CEO,77 Baker Blvd,Woodport,GA,30310,USA,555-825-2555,48 -CUST006,FastFix Auto,Isabelle Dubois,Service Manager,42 Garage Loop,Montréal,QC,H3Z 2Y7,Canada,514-601-3300,60 -CUST007,Garden Glow Ltd,Ellie Tan,Product Specialist,180 Daisy St,Burnside,NSW,2135,Australia,02-8000-1122,27 -CUST008,Evergreen Crafts,Jasper Lee,Owner,96 Willow Way,Lakeside,BC,V2V 4W1,Canada,604-800-9988,43 -CUST009,Peak Fitness Inc,Monica Anders,Marketing Lead,311 Summit Ave,Cascade,WA,98101,USA,206-777-5544,56 -CUST010,Silverline Media,Nikhil Ajay,Account Executive,845 Market Plaza,Redhill,ENG,RH1 6JT,UK,020-7123-4098,19 diff --git a/Babylon/templates/working_dir/.templates/yaml/kob/Webapp.yaml b/Babylon/templates/working_dir/.templates/yaml/kob/Webapp.yaml new file mode 100644 index 000000000..134fc24f9 --- /dev/null +++ b/Babylon/templates/working_dir/.templates/yaml/kob/Webapp.yaml @@ -0,0 +1,14 @@ +kind: Webapp +namespace: + remote: {{remote}} +spec: + payload: + cloud_provider: "{{cloud_provider}}" + cluster_name: "{{cluster_name}}" + domain_zone: "{{domain_zone}}" + tenant: "{{tenant}}" + webapp_name: "{{webapp_name}}" + azure_entra_tenant_id: "{{azure_entra_tenant_id}}" + organization_id: "{{services['api.organization_id']}}" + state_host: "{{state_host}}" + powerbi_app_deploy: {{powerbi_app_deploy}} \ No newline at end of file diff --git a/Babylon/templates/working_dir/.templates/yaml/kob/variables.yaml b/Babylon/templates/working_dir/.templates/yaml/kob/variables.yaml new file mode 100644 index 000000000..3c8c508b3 --- /dev/null +++ b/Babylon/templates/working_dir/.templates/yaml/kob/variables.yaml @@ -0,0 +1,45 @@ +# ========================================================= +# IMPORTANT: You can add variables here as needed! +# Make sure they are used in the manifest YAML. +# ========================================================= +# Organization +organization_name: Babylon v5 Organization +# Solution +solution_name: Babylon v5 Solution +simulator_repository: tenant-sphinx/brewerysamplesolution_simulator +solution_description: Brewery Testing babylon v5 Solution PLT +solution_key: breweryTesting +simulator_version: 3.0.0 +# Workspace +workspace_name: Babylon v5 workspace +workspace_key: brewerytestingwork +workspace_description: Testing workspace for the brewery web application +# Webapp +# These variables are used to render your Webapp.yaml manifest and can be mapped to the +# terraform module variables (terraform.tfvars) used by the terraform-webapp module. +# See the module's example tfvars for reference: +# https://github.com/Cosmo-Tech/terraform-webapp/blob/main/terraform.tfvars + +## VARIABLES EXAMPLE FOR KOB (= On-Premise) +cloud_provider: kob +cluster_name: kubernetes +domain_zone: onpremise.platform.cosmotech.com +tenant: sphinx +webapp_name: business +organization_id: o-xxxxxxxxxxxx +azure_entra_tenant_id: e413b834-8be8-4822-a370-be619545cb49 +state_host: https://cosmotechstates.onpremise.platform.cosmotech.com + +# Remote state +remote: true + +# ACL +security: + default: none + accessControlList: + - id: tenant-admin + role: admin + - id: tenant-editor + role: editor + - id: tenant-viewer + role: viewer \ No newline at end of file diff --git a/Babylon/templates/working_dir/terraform_cloud/tfc_variables_create.yaml b/Babylon/templates/working_dir/terraform_cloud/tfc_variables_create.yaml deleted file mode 100644 index 3476453bf..000000000 --- a/Babylon/templates/working_dir/terraform_cloud/tfc_variables_create.yaml +++ /dev/null @@ -1,24 +0,0 @@ -- key: var_key, - value: var_value, - description: var_description, - # category: var_category, - # hcl: var_hcl, - # sensitive: var_sensitive -- key: var_key, - value: var_value, - description: var_description, - # category: var_category, - # hcl: var_hcl, - # sensitive: var_sensitive -- key: var_key, - value: var_value, - description: var_description, - # category: var_category, - # hcl: var_hcl, - # sensitive: var_sensitive -- key: var_key, - value: var_value, - description: var_description, - # category: var_category, - # hcl: var_hcl, - # sensitive: var_sensitive \ No newline at end of file diff --git a/Babylon/templates/working_dir/terraform_cloud/tfc_workspace_create.yaml b/Babylon/templates/working_dir/terraform_cloud/tfc_workspace_create.yaml deleted file mode 100644 index 1bb6ff10b..000000000 --- a/Babylon/templates/working_dir/terraform_cloud/tfc_workspace_create.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# workspace_name: name of the workspace -workspace_name: "" -# working_directory: working directory of the workspace -working_directory: "" -# vcs_identifier: identifier of the vcs associated to the workspace -vcs_identifier: "" -# vcs_branch: branch in the vcs used by the workspace -vcs_branch: "" -# vcs_oauth_token_id: id to the oauth token used to connect to the vcs -vcs_oauth_token_id: "" \ No newline at end of file diff --git a/Babylon/utils/decorators.py b/Babylon/utils/decorators.py index 6708c2902..e661f6b0f 100644 --- a/Babylon/utils/decorators.py +++ b/Babylon/utils/decorators.py @@ -188,12 +188,6 @@ def wrap_function(func: Callable[..., Any]) -> Callable[..., Any]: "tenant", help="Tenant Id without any special character", ) - @option( - "-s", - "--state-id", - "state_id", - help="State Id", - ) @wraps(func) def wrapper(*args: Any, **kwargs: Any): context = kwargs.pop("context", None) @@ -202,10 +196,7 @@ def wrapper(*args: Any, **kwargs: Any): tenant = kwargs.pop("tenant", None) if tenant and check_special_char(string=tenant): env.set_environ(tenant) - state_id = kwargs.pop("state_id", None) - if state_id and check_special_char(string=state_id): - env.set_state_id(state_id) - env.get_namespace_from_local(context=context, tenant=tenant, state_id=state_id) + env.get_namespace_from_local(context=context, tenant=tenant) return func(*args, **kwargs) return wrapper @@ -235,9 +226,20 @@ def wrapper(*args: Any, **kwargs: Any): def wrapcontext() -> Callable[..., Any]: def wrap_function(func: Callable[..., Any]) -> Callable[..., Any]: - @option("-c", "--context", "context", required=True, help="Context Name") - @option("-t", "--tenant", "tenant", required=True, help="Tenant Name") - @option("-s", "--state-id", "state_id", required=True, help="State Id") + @option( + "-c", + "--context", + "context", + required=True, + help="A unique identifier to isolate the project state (e.g., 'feature-x', 'prod-v1').", + ) + @option( + "-t", + "--tenant", + "tenant", + required=True, + help="The tenant name (Kubernetes namespace) where the project will be deployed.", + ) @wraps(func) def wrapper(*args: Any, **kwargs: Any): context = kwargs.pop("context", None) @@ -246,9 +248,6 @@ def wrapper(*args: Any, **kwargs: Any): tenant = kwargs.pop("tenant", None) if tenant and check_special_char(string=tenant): env.set_environ(tenant) - state_id = kwargs.pop("state_id", None) - if state_id and check_special_char(string=state_id): - env.set_state_id(state_id) return func(*args, **kwargs) return wrapper diff --git a/Babylon/utils/environment.py b/Babylon/utils/environment.py index 8d1616f97..3cde57247 100644 --- a/Babylon/utils/environment.py +++ b/Babylon/utils/environment.py @@ -6,7 +6,6 @@ from logging import getLogger from pathlib import Path -from azure.storage.blob import BlobServiceClient from flatten_json import flatten from kubernetes import client, config from kubernetes.client.exceptions import ApiException @@ -15,6 +14,7 @@ from yaml import SafeLoader, YAMLError, dump, load, safe_load from Babylon.utils import ORIGINAL_CONFIG_FOLDER_PATH, ORIGINAL_TEMPLATE_FOLDER_PATH +from Babylon.utils.kubernetes_state import STATE_LABEL_KEY, STATE_LABEL_VALUE, retrieve_state_from_kubernetes, save_state_in_kubernetes from Babylon.utils.working_dir import WorkingDir from Babylon.utils.yaml_utils import yaml_to_json @@ -23,6 +23,7 @@ STORE_STRING = "datastore" TEMPLATES_STRING = "templates" PATH_SYMBOL = "%" +NAMESPACE_FILE = "namespace.yaml" class SingletonMeta(type): @@ -53,7 +54,6 @@ def __init__(self): self.remote = False self.pwd = Path.cwd() self.blob_client = None - self.state_id: str = "" self.context_id: str = "" self.environ_id: str = "" self.server_id: str = "" @@ -71,10 +71,6 @@ def __init__(self): self.working_dir = WorkingDir(working_dir_path=self.pwd) self.variable_files: list[Path] = [] - def _get_state_blob_client(self, blob_name: str): - """Get a blob client for state management""" - return self.blob_client.get_blob_client(container=self.STATE_CONTAINER, blob=blob_name) - def get_variables(self): merged_data, duplicate_keys = self.merge_yaml_files(self.variable_files) if len(duplicate_keys) > 0: @@ -96,8 +92,6 @@ def get_ns_from_text(self, content: str): payload_dict = safe_load(payload) remote: bool = payload_dict.get("remote", self.remote) self.remote = remote - if remote: - self.set_blob_client() def fill_template(self, data: str, state: dict = None, ext_args: dict = None): result = data.replace("{{", "${").replace("}}", "}") @@ -119,80 +113,42 @@ def set_context(self, context_id): def set_environ(self, environ_id): self.environ_id = environ_id - def set_state_id(self, state_id: str): - self.state_id = state_id - - def set_blob_client(self): + def _get_active_kubectl_context(self) -> str: try: - storage_name = os.getenv("STORAGE_NAME", "").strip() - account_secret = os.getenv("ACCOUNT_SECRET", "").strip() - if not storage_name and not account_secret: - raise EnvironmentError("Missing environment variables: 'STORAGE_NAME' and 'ACCOUNT_SECRET'") - connection_str = ( - f"DefaultEndpointsProtocol=https;" - f"AccountName={storage_name};" - f"AccountKey={account_secret};" - f"EndpointSuffix=core.windows.net" - ) - self.blob_client = BlobServiceClient.from_connection_string(connection_str) - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to initialize BlobServiceClient: {e}") - sys.exit(1) + from kubernetes.config.kube_config import list_kube_config_contexts - def get_config_from_k8s_secret_by_tenant(self, secret_name: str, tenant: str): - response_parsed = {} + _, active_context = list_kube_config_contexts() + return active_context["name"] if active_context else "unknown" + except Exception: + return "unknown" + + def _get_babylon_namespace_info(self) -> str: + ns_file = self.state_dir / NAMESPACE_FILE + if not ns_file.exists(): + return "[dim]not set[/dim]" try: - config.load_kube_config() - except ConfigException as e: - logger.error("\n [bold red]✘[/bold red] Failed to load kube config") - logger.error(f" [red]Reason:[/red] {e}") - logger.info("\n [bold white]💡 Troubleshooting:[/bold white]") - logger.info(" • Ensure your kubeconfig file is valid") - logger.info(" • Set your context: [cyan]kubectl config use-context [/cyan]") - sys.exit(1) + ns_data = safe_load(ns_file.open("r").read()) or {} + babylon_ctx = ns_data.get("context", "") + babylon_tenant = ns_data.get("tenant", "") + return f"context=[bold cyan]{babylon_ctx}[/bold cyan] tenant=[bold cyan]{babylon_tenant}[/bold cyan] " + except Exception: + return "[dim]unavailable[/dim]" + + def _load_k8s_secret(self, secret_name: str, tenant: str): try: v1 = client.CoreV1Api() - secret = v1.read_namespaced_secret(name=secret_name, namespace=tenant) + return v1.read_namespaced_secret(name=secret_name, namespace=tenant) except ApiException: - logger.error("\n [bold red]✘[/bold red] Resource Not Found") - logger.error(f" Secret [green]{secret_name}[/green] could not be found in namespace [green]{tenant}[/green]") - - # Show current kubectl context to help users spot a misconfiguration - try: - from kubernetes.config.kube_config import list_kube_config_contexts - - _, active_context = list_kube_config_contexts() - current_k8s_ctx = active_context["name"] if active_context else "unknown" - except Exception: - current_k8s_ctx = "unknown" - - # Show current Babylon namespace from local config - ns_file = self.state_dir / "namespace.yaml" - if ns_file.exists(): - try: - ns_data = safe_load(ns_file.open("r").read()) or {} - babylon_ctx = ns_data.get("context", "") - babylon_tenant = ns_data.get("tenant", "") - babylon_state = ns_data.get("state_id", "") - babylon_ns_info = ( - f"context=[bold cyan]{babylon_ctx}[/bold cyan] " - f"tenant=[bold cyan]{babylon_tenant}[/bold cyan] " - f"state-id=[bold cyan]{babylon_state}[/bold cyan]" - ) - except Exception: - babylon_ctx = babylon_tenant = babylon_state = "" - babylon_ns_info = "[dim]unavailable[/dim]" - else: - babylon_ctx = babylon_tenant = babylon_state = "" - babylon_ns_info = "[dim]not set[/dim]" - + logger.error( + f" [yellow]⚠[/yellow] Secret [green]{secret_name}[/green] could not be found in namespace [green]{tenant}[/green]." + ) logger.info("\n [bold white]💡 Troubleshooting:[/bold white]") - logger.info(f" • Active kubectl context : [cyan]{current_k8s_ctx}[/cyan]") - logger.info(f" • Active Babylon namespace: {babylon_ns_info}") + logger.info(f" • Active kubectl context : [cyan]{self._get_active_kubectl_context()}[/cyan]") + logger.info(f" • Active Babylon namespace: {self._get_babylon_namespace_info()}") logger.info(" • If the kubectl context is wrong, switch it:") logger.info(" [cyan]kubectl config use-context [/cyan]") logger.info(" • If the Babylon namespace is wrong, switch it:") - logger.info(" [cyan]babylon namespace use -c -t -s [/cyan]") + logger.info(" [cyan]babylon namespace use -c -t [/cyan]") sys.exit(1) except Exception: logger.error( @@ -200,48 +156,51 @@ def get_config_from_k8s_secret_by_tenant(self, secret_name: str, tenant: str): "'Cluster may be down, kube-apiserver unreachable'" ) sys.exit(1) - if secret.data: - for key, value in secret.data.items(): - decoded_value = b64decode(value).decode("utf-8") - response_parsed[key] = decoded_value - else: + + def get_config_from_k8s_secret_by_tenant(self, secret_name: str, tenant: str): + try: + config.load_kube_config() + except ConfigException as e: + logger.error("\n [bold red]✘[/bold red] Failed to load kube config") + logger.error(f" [red]Reason:[/red] {e}") + logger.info("\n [bold white]💡 Troubleshooting:[/bold white]") + logger.info(" • Ensure your kubeconfig file is valid") + logger.info(" • Set your context: [cyan]kubectl config use-context [/cyan]") + sys.exit(1) + + secret = self._load_k8s_secret(secret_name, tenant) + + if not secret.data: logger.warning(f" [yellow]⚠[/yellow] Secret {secret_name} in namespace '{tenant}' has no data") - return response_parsed + return {} + + return {key: b64decode(value).decode("utf-8") for key, value in secret.data.items()} def store_state_in_local(self, state: dict): - state_file = f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" + state_file = f"state.{self.context_id}.{self.environ_id}.yaml" self.state_dir.mkdir(parents=True, exist_ok=True) s = self.state_dir / state_file - state["files"] = self.working_dir.files_to_deploy s.write_bytes(data=dump(state).encode("utf-8")) - def store_state_in_cloud(self, state: dict): - state_file = f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" - state_container = self.blob_client.get_container_client(container=self.STATE_CONTAINER) - if not state_container.exists(): - state_container.create_container() - state_blob = self._get_state_blob_client(state_file) - if state_blob.exists(): - state_blob.delete_blob() - state_blob.upload_blob(data=dump(state).encode("utf-8")) + def store_state_in_kubernetes(self, state: dict, namespace: str = "", secret_name: str = "") -> None: + """Persist *state* as a Kubernetes Secret.""" + ns = namespace or self.environ_id + name = secret_name or f"babylon-state-{self.context_id}-{self.environ_id}" + save_state_in_kubernetes(namespace=ns, secret_name=name, state_data=state) - def list_remote_states(self) -> list[str]: - """List state file names present in the Azure blob container.""" - try: - self.set_blob_client() - container_client = self.blob_client.get_container_client(container=self.STATE_CONTAINER) - blobs = container_client.list_blobs(name_starts_with="state.") - return [b.name for b in blobs if b.name.endswith(".yaml")] - except Exception as e: - logger.error(f" [bold red]✘[/bold red] Failed to list remote states: {e}") - return [] + def get_state_from_kubernetes(self, namespace: str = "", secret_name: str = "") -> dict: + """Retrieve state from a Kubernetes Secret. - def get_state_from_local(self): - state_file = self.state_dir / f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" - if not state_file.exists(): + Returns the stored dictionary, or an empty default state when the + secret does not exist yet (mirrors the behaviour of + ``get_state_from_local`` and ``get_state_from_kubernetes``). + """ + ns = namespace or self.environ_id + name = secret_name or f"babylon-state-{self.context_id}-{self.environ_id}" + result = retrieve_state_from_kubernetes(namespace=ns, secret_name=name) + if result is None: return { "context": self.context_id, - "id": self.state_id, "tenant": self.environ_id, "remote": self.remote, "services": { @@ -259,17 +218,31 @@ def get_state_from_local(self): }, }, } - state_data = load(state_file.open("r"), Loader=SafeLoader) - return state_data + return result + + def list_remote_states(self) -> list[str]: + """List state secret names matching 'babylon-state-*' in the current namespace. + + Uses a server-side label selector so only matching secrets are transferred + over the wire — no client-side filtering needed. + """ + try: + config.load_kube_config() + v1 = client.CoreV1Api() + secrets = v1.list_namespaced_secret( + namespace=self.environ_id, + label_selector=f"{STATE_LABEL_KEY}={STATE_LABEL_VALUE}", + ) + return [s.metadata.name for s in secrets.items] + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Failed to list remote states: {e}") + return [] - def get_state_from_cloud(self) -> dict: - s = f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" - state_blob = self._get_state_blob_client(s) - exists = state_blob.exists() - if not exists: + def get_state_from_local(self): + state_file = self.state_dir / f"state.{self.context_id}.{self.environ_id}.yaml" + if not state_file.exists(): return { "context": self.context_id, - "id": self.state_id, "tenant": self.environ_id, "remote": self.remote, "services": { @@ -287,22 +260,21 @@ def get_state_from_cloud(self) -> dict: }, }, } - data = load(state_blob.download_blob().readall(), Loader=SafeLoader) - return data + state_data = load(state_file.open("r"), Loader=SafeLoader) + return state_data def store_namespace_in_local(self): ns_dir = self.state_dir if not ns_dir.exists(): ns_dir.mkdir(parents=True, exist_ok=True) - s = ns_dir / "namespace.yaml" - ns = {"state_id": self.state_id, "context": self.context_id, "tenant": self.environ_id} + s = ns_dir / NAMESPACE_FILE + ns = {"context": self.context_id, "tenant": self.environ_id} s.write_bytes(data=dump(ns).encode("utf-8")) - self.set_state_id(state_id=self.state_id) self.set_context(context_id=self.context_id) self.set_environ(environ_id=self.environ_id) - def get_namespace_from_local(self, context: str = "", tenant: str = "", state_id: str = ""): - ns_file = self.state_dir / "namespace.yaml" + def get_namespace_from_local(self, context: str = "", tenant: str = ""): + ns_file = self.state_dir / NAMESPACE_FILE if not ns_file.exists(): logger.error(f" [bold red]✘[/bold red] [cyan]{ns_file}[/cyan] not found") logger.info(" Run the following command to set your active namespace:") @@ -313,8 +285,6 @@ def get_namespace_from_local(self, context: str = "", tenant: str = "", state_id if ns_data: self.context_id = context or ns_data.get("context", "") self.environ_id = tenant or ns_data.get("tenant", "") - self.state_id = state_id or ns_data.get("state_id", "") - self.set_state_id(state_id=self.state_id) return ns_data def retrieve_config(self): @@ -340,7 +310,7 @@ def retrieve_config(self): def retrieve_state_func(self): if self.remote: - state = self.get_state_from_cloud() + state = self.get_state_from_kubernetes() else: state = self.get_state_from_local() return state diff --git a/Babylon/utils/kubernetes_state.py b/Babylon/utils/kubernetes_state.py new file mode 100644 index 000000000..8bde23694 --- /dev/null +++ b/Babylon/utils/kubernetes_state.py @@ -0,0 +1,154 @@ +""" +Cloud-agnostic Kubernetes Secret state management. + +Works with any Kubernetes cluster (AKS, EKS, GKE, KOB/on-prem, …) that is +reachable via the current kubeconfig context. + +Secret layout +───────────── + apiVersion: v1 + kind: Secret + type: Opaque + metadata: + name: + namespace: + data: + state.yaml: + +The single key inside the secret is always ``STATE_KEY`` ("state.yaml"). +""" + +import sys +from base64 import b64decode, b64encode +from logging import getLogger + +from kubernetes import client, config +from kubernetes.client.exceptions import ApiException +from kubernetes.config.config_exception import ConfigException +from yaml import dump, safe_load + +logger = getLogger(__name__) + +# The key name stored inside the Kubernetes Secret's data map. +STATE_KEY = "state.yaml" +# Label applied to every Babylon state secret enables fast server-side listing. +STATE_LABEL_KEY = "app.kubernetes.io/managed-by" +STATE_LABEL_VALUE = "babylon-state" + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +def _load_kube_config() -> None: + """Load kubeconfig, with a clear error message on failure.""" + try: + config.load_kube_config() + except ConfigException as exc: + logger.error("\n [bold red]✘[/bold red] Failed to load kube config") + logger.error(f" [red]Reason:[/red] {exc}") + logger.info("\n [bold white]💡 Troubleshooting:[/bold white]") + logger.info(" • Ensure your kubeconfig file is valid") + logger.info(" • Set your context: [cyan]kubectl config use-context [/cyan]") + sys.exit(1) + + +def _core_v1() -> client.CoreV1Api: + """Return a CoreV1Api instance (kubeconfig must already be loaded).""" + return client.CoreV1Api() + + +def _encode(data: dict) -> str: + """Serialise *data* to YAML and return a base64 string (utf-8).""" + yaml_str = dump(data, allow_unicode=True) + return b64encode(yaml_str.encode("utf-8")).decode("utf-8") + + +def _decode(raw: bytes | str) -> dict: + """Decode a base64 value coming from a Secret's ``data`` field. + """ + if isinstance(raw, (bytes, bytearray)): + yaml_str = raw.decode("utf-8") + else: + # Still base64-encoded (older client versions or raw JSON payload). + yaml_str = b64decode(raw).decode("utf-8") + return safe_load(yaml_str) or {} + + +def _build_secret(namespace: str, secret_name: str, encoded_value: str) -> client.V1Secret: + """Build a V1Secret object ready for create / replace calls.""" + return client.V1Secret( + api_version="v1", + kind="Secret", + type="Opaque", + metadata=client.V1ObjectMeta( + name=secret_name, + namespace=namespace, + labels={STATE_LABEL_KEY: STATE_LABEL_VALUE}, + ), + data={STATE_KEY: encoded_value}, + ) + + +# Public API + + +def save_state_in_kubernetes(namespace: str, secret_name: str, state_data: dict) -> None: + """Persist *state_data* as a Kubernetes Secret in *namespace*. + """ + _load_kube_config() + v1 = _core_v1() + encoded = _encode(state_data) + secret = _build_secret(namespace, secret_name, encoded) + + try: + existing_secret = v1.read_namespaced_secret(name=secret_name, namespace=namespace) + if existing_secret.metadata and secret.metadata: + secret.metadata.resource_version = existing_secret.metadata.resource_version + v1.replace_namespaced_secret(name=secret_name, namespace=namespace, body=secret) + logger.info(f" [green]✔[/green] State secret [cyan]{secret_name}[/cyan] updated in namespace [cyan]{namespace}[/cyan]") + except ApiException as exc: + if exc.status == 404: + # Secret does not exist → create it. + v1.create_namespaced_secret(namespace=namespace, body=secret) + logger.info(f" [green]✔[/green] State secret [cyan]{secret_name}[/cyan] created in namespace [cyan]{namespace}[/cyan]") + else: + logger.error(f" [bold red]✘[/bold red] Kubernetes API error while storing state (HTTP {exc.status}): {exc.reason}") + sys.exit(1) + except Exception as exc: + logger.error(f" [bold red]✘[/bold red] Failed to connect to the Kubernetes cluster: {exc}") + sys.exit(1) + + +def retrieve_state_from_kubernetes(namespace: str, secret_name: str) -> dict | None: + """Read state from a Kubernetes Secret and return it as a dictionary. + + Returns ``None`` when the secret does not exist so the caller can decide + whether to initialise a fresh state or raise an error. + """ + _load_kube_config() + v1 = _core_v1() + + try: + secret = v1.read_namespaced_secret(name=secret_name, namespace=namespace) + except ApiException as exc: + if exc.status == 404: + logger.warning( + f" [yellow]⚠[/yellow] State secret [cyan]{secret_name}[/cyan] not found in namespace [cyan]{namespace}[/cyan]" + ) + return None + logger.error(f" [bold red]✘[/bold red] Kubernetes API error while retrieving state (HTTP {exc.status}): {exc.reason}") + sys.exit(1) + except Exception as exc: + logger.error(f" [bold red]✘[/bold red] Failed to connect to the Kubernetes cluster: {exc}") + sys.exit(1) + + if not secret.data or STATE_KEY not in secret.data: + logger.warning( + f" [yellow]⚠[/yellow] State secret [cyan]{secret_name}[/cyan] exists but contains no [cyan]{STATE_KEY}[/cyan] key" + ) + return None + + state = _decode(secret.data[STATE_KEY]) + logger.info(f" [green]✔[/green] State loaded from secret [cyan]{secret_name}[/cyan] in namespace [cyan]{namespace}[/cyan]") + return state diff --git a/Babylon/version.py b/Babylon/version.py index 424e92ffd..6f6b6753f 100644 --- a/Babylon/version.py +++ b/Babylon/version.py @@ -1,4 +1,4 @@ -VERSION = "5.1.0" +VERSION = "5.2.0" def get_version(): diff --git a/pyproject.toml b/pyproject.toml index 8774cc75f..6353c467f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,14 +15,12 @@ dependencies = [ "click-log", "cryptography", "pyyaml", - "requests>=2.32.5", + "requests>=2.33.1", "rich", "Mako", "polling2", - "inquirer", "flatten_json", "dynaconf", - "azure-mgmt-authorization>=4.0.0", "kubernetes>=35.0.0", "cosmotech-api==5.0.1" ] diff --git a/tests/e2e/test_e2e.sh b/tests/e2e/test_e2e.sh index 19e5fd464..ca9930276 100755 --- a/tests/e2e/test_e2e.sh +++ b/tests/e2e/test_e2e.sh @@ -11,15 +11,14 @@ fi # Set testing namespace export CONTEXT="e2e" export TENANT="tenant-sphinx" -export STATE="teststate" -babylon namespace use -c ${CONTEXT} -t ${TENANT} -s $STATE +babylon namespace use -c ${CONTEXT} -t ${TENANT} babylon namespace get-states local babylon namespace get-contexts # Get version babylon api about -babylon init +babylon init azure babylon apply --exclude webapp project babylon destroy \ No newline at end of file diff --git a/tests/integration/test_api_endpoints.sh b/tests/integration/test_api_endpoints.sh index f5ec73d71..7e277d127 100755 --- a/tests/integration/test_api_endpoints.sh +++ b/tests/integration/test_api_endpoints.sh @@ -13,9 +13,8 @@ mkdir output # Set testing namespace export CONTEXT="integration" export TENANT="tenant-sphinx" -export STATE="teststate" -babylon namespace use -c ${CONTEXT} -t ${TENANT} -s $STATE +babylon namespace use -c ${CONTEXT} -t ${TENANT} babylon namespace get-states local babylon namespace get-contexts diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 8714b1cea..8bb8955ae 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -4,7 +4,7 @@ from cosmotech_api.models.solution_access_control import SolutionAccessControl from cosmotech_api.models.workspace_access_control import WorkspaceAccessControl -from Babylon.commands.macro.deploy import diff, resolve_inclusion_exclusion +from Babylon.commands.macro.helpers.common import diff, resolve_inclusion_exclusion def test_organization_diff(): diff --git a/uv.lock b/uv.lock index e4a65c8a0..43a881a38 100644 --- a/uv.lock +++ b/uv.lock @@ -11,24 +11,6 @@ wheels = [ { 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 = "ansicon" -version = "1.89.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/e2/1c866404ddbd280efedff4a9f15abfe943cb83cde6e895022370f3a61f85/ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", size = 67312, upload-time = "2019-04-29T20:23:57.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675, upload-time = "2019-04-29T20:23:53.83Z" }, -] - -[[package]] -name = "azure-common" -version = "1.1.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914, upload-time = "2022-02-03T19:39:44.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462, upload-time = "2022-02-03T19:39:42.417Z" }, -] - [[package]] name = "azure-core" version = "1.39.0" @@ -58,20 +40,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, ] -[[package]] -name = "azure-mgmt-authorization" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-common" }, - { name = "azure-mgmt-core" }, - { name = "isodate" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/ab/e79874f166eed24f4456ce4d532b29a926fb4c798c2c609eefd916a3f73d/azure-mgmt-authorization-4.0.0.zip", hash = "sha256:69b85abc09ae64fc72975bd43431170d8c7eb5d166754b98aac5f3845de57dc4", size = 1134795, upload-time = "2023-07-25T04:47:46.033Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/b3/8ec1268082f4d20cc8bf723a1a8e6b9e330bcc338a4dbcee9c7737e9dc1c/azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a", size = 1072620, upload-time = "2023-07-25T04:47:49.26Z" }, -] - [[package]] name = "azure-mgmt-core" version = "1.6.0" @@ -127,7 +95,6 @@ name = "babylon" source = { editable = "." } dependencies = [ { name = "azure-identity" }, - { name = "azure-mgmt-authorization" }, { name = "azure-mgmt-storage" }, { name = "azure-storage-blob" }, { name = "click" }, @@ -136,7 +103,6 @@ dependencies = [ { name = "cryptography" }, { name = "dynaconf" }, { name = "flatten-json" }, - { name = "inquirer" }, { name = "kubernetes" }, { name = "mako" }, { name = "polling2" }, @@ -164,21 +130,19 @@ doc = [ [package.metadata] requires-dist = [ { name = "azure-identity" }, - { name = "azure-mgmt-authorization", specifier = ">=4.0.0" }, { name = "azure-mgmt-storage" }, { name = "azure-storage-blob", specifier = ">=12.28.0" }, { name = "click" }, { name = "click-log" }, - { name = "cosmotech-api", specifier = "==5.0.0" }, + { name = "cosmotech-api", specifier = "==5.0.1" }, { name = "cryptography" }, { name = "dynaconf" }, { name = "flatten-json" }, - { name = "inquirer" }, { name = "kubernetes", specifier = ">=35.0.0" }, { name = "mako" }, { name = "polling2" }, { name = "pyyaml" }, - { name = "requests", specifier = ">=2.32.5" }, + { name = "requests", specifier = ">=2.33.1" }, { name = "rich" }, ] @@ -212,26 +176,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] -[[package]] -name = "blessed" -version = "1.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinxed", marker = "sys_platform == 'win32'" }, - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/5c/92dc10a25a4eafb4b9bef5dad522a0b7d5d5b55d2d76f9a6721b2e49ca2c/blessed-1.33.0.tar.gz", hash = "sha256:c732a1043042d84f411423a1a7b74643e1dd3a2271bd6e5955682dd4a321b0ef", size = 13980368, upload-time = "2026-03-07T00:00:06.288Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/04/2b4e111e0b902b1ac0b25e5e010af71c79fca093a3399bd7f8b82adcc536/blessed-1.33.0-py3-none-any.whl", hash = "sha256:1bc8ecac6d139286ea51ec1683433528ce75b0c60db77b7d881112bf9fc85b0f", size = 111519, upload-time = "2026-03-07T00:00:00.202Z" }, -] - [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] @@ -293,87 +244,87 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, - { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, - { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, - { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, - { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, - { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -399,7 +350,7 @@ wheels = [ [[package]] name = "cosmotech-api" -version = "5.0.0" +version = "5.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, @@ -407,62 +358,62 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/07/4cfc51935a2cf9278f6f51d1fb59b402c26895dafc5923fdebe99f77afcb/cosmotech_api-5.0.0.tar.gz", hash = "sha256:182b6801a4a5008feeb7e20b367c5420005421978ef79e69edeacf7495456d5a", size = 219103, upload-time = "2026-02-23T15:50:44.042Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/fe/63d70c38d0c6b1a0319bf0b967883c4ff1ca3146e1c9933d3986eb84eb4f/cosmotech_api-5.0.1.tar.gz", hash = "sha256:f399350288e8708074bdd65e32e35adee139ef00dd3daf71789b7a54453f524a", size = 218883, upload-time = "2026-03-26T11:22:47.705Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/7e/1294914773dd6407395d22ae9734a68455443d55e526ceaff3d9a08b0b3c/cosmotech_api-5.0.0-py3-none-any.whl", hash = "sha256:1e41ea2c9ab09ea678e1861e5751c6976430989f51f34496dce4b8e1772c0b0f", size = 625091, upload-time = "2026-02-23T15:50:42.546Z" }, + { url = "https://files.pythonhosted.org/packages/05/0c/a2170429bcc84061422c8c2f95d839312cbaaaebfd902af15b959945c15c/cosmotech_api-5.0.1-py3-none-any.whl", hash = "sha256:91338003e8ee7fe4444d24d8af1c8601b715ebb6e1dfe258758d34cf718350b7", size = 624861, upload-time = "2026-03-26T11:22:46.079Z" }, ] [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] @@ -483,19 +434,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/43/11d6e5d2c00bf000b5329717c74563bf76a9193f4a41cb0c4ef277dde4fa/dynaconf-3.2.13-py2.py3-none-any.whl", hash = "sha256:4305527aef4834bdba3e39479b23c005186e83fb85f65bcaa4bcea58fa26759b", size = 238041, upload-time = "2026-03-17T19:38:45.337Z" }, ] -[[package]] -name = "editor" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "runs" }, - { name = "xmod" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/4f/00e0b75d86bb1e6a943c08942619e3f31de54a0dce3b33b14ae3c2af2dc0/editor-1.7.0.tar.gz", hash = "sha256:979b25e3f7e0386af4478e7392ecb99e6c16a42db7c4336d6b16658fa0449fb3", size = 2355, upload-time = "2026-02-03T13:51:30.717Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/b5/f566c215c58d7d2b8d39104b6cda00f31a18bb480486cb7f0d68de6131f9/editor-1.7.0-py3-none-any.whl", hash = "sha256:8b1ad5e99846b076b96b18f7bc39ae21952c8e20d375c3f8f98fd02cacf19367", size = 3383, upload-time = "2026-02-03T13:51:29.075Z" }, -] - [[package]] name = "flatten-json" version = "0.1.14" @@ -522,20 +460,20 @@ wheels = [ [[package]] name = "griffelib" -version = "2.0.1" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/d7/2b805e89cdc609e5b304361d80586b272ef00f6287ee63de1e571b1f71ec/griffelib-2.0.1.tar.gz", hash = "sha256:59f39eabb4c777483a3823e39e8f9e03e69df271a7e49aee64e91a8cfa91bdf5", size = 166383, upload-time = "2026-03-23T21:05:25.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl", hash = "sha256:b769eed581c0e857d362fc8fcd8e57ecd2330c124b6104ac8b4c1c86d76970aa", size = 142377, upload-time = "2026-03-23T21:04:01.116Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] @@ -547,20 +485,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "inquirer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blessed" }, - { name = "editor" }, - { name = "readchar" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/79/165579fdcd3c2439503732ae76394bf77f5542f3dd18135b60e808e4813c/inquirer-3.4.1.tar.gz", hash = "sha256:60d169fddffe297e2f8ad54ab33698249ccfc3fc377dafb1e5cf01a0efb9cbe5", size = 14069, upload-time = "2025-08-02T18:36:27.901Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/fd/7c404169a3e04a908df0644893a331f253a7f221961f2b6c0cf44430ae5a/inquirer-3.4.1-py3-none-any.whl", hash = "sha256:717bf146d547b595d2495e7285fd55545cff85e5ce01decc7487d2ec6a605412", size = 18152, upload-time = "2025-08-02T18:36:26.753Z" }, -] - [[package]] name = "isodate" version = "0.7.2" @@ -582,18 +506,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jinxed" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ansicon", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/20/d0/59b2b80e7a52d255f9e0ad040d2e826342d05580c4b1d7d7747cfb8db731/jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf", size = 80981, upload-time = "2024-07-31T22:39:18.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085, upload-time = "2024-07-31T22:39:17.426Z" }, -] - [[package]] name = "kubernetes" version = "35.0.0" @@ -616,14 +528,14 @@ wheels = [ [[package]] name = "mako" -version = "1.3.10" +version = "1.3.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/8a/805404d0c0b9f3d7a326475ca008db57aea9c5c9f2e1e39ed0faa335571c/mako-1.3.11.tar.gz", hash = "sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069", size = 399811, upload-time = "2026-04-14T20:19:51.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://files.pythonhosted.org/packages/68/a5/19d7aaa7e433713ffe881df33705925a196afb9532efc8475d26593921a6/mako-1.3.11-py3-none-any.whl", hash = "sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77", size = 78503, upload-time = "2026-04-14T20:19:53.233Z" }, ] [[package]] @@ -730,7 +642,7 @@ wheels = [ [[package]] name = "mike" -version = "2.1.4" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -740,9 +652,9 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "verspec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/09/de1cab0018eb5f1fbd9dcc26b6e61f9453c5ec2eb790949d6ed75e1ffe55/mike-2.1.4.tar.gz", hash = "sha256:75d549420b134603805a65fc67f7dcd9fcd0ad1454fb2c893d9e844cba1aa6e4", size = 38190, upload-time = "2026-03-08T02:46:29.187Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/47/fa87e9d56bef16cdfe34b059a437e8c6f7ec6f1b9c378871c3cf95ebea9c/mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e", size = 38450, upload-time = "2026-04-14T04:59:03.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl", hash = "sha256:39933e992e155dd70f2297e749a0ed78d8fd7942bc33a3666195d177758a280e", size = 33820, upload-time = "2026-03-08T02:46:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040", size = 34026, upload-time = "2026-04-14T04:59:02.602Z" }, ] [[package]] @@ -856,7 +768,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -866,9 +778,9 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, ] [package.optional-dependencies] @@ -892,16 +804,16 @@ wheels = [ [[package]] name = "msal" -version = "1.35.1" +version = "1.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/aa/5a646093ac218e4a329391d5a31e5092a89db7d2ef1637a90b82cd0b6f94/msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656", size = 165658, upload-time = "2026-03-04T23:38:51.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cb/b02b0f748ac668922364ccb3c3bff5b71628a05f5adfec2ba2a5c3031483/msal-1.36.0.tar.gz", hash = "sha256:3f6a4af2b036b476a4215111c4297b4e6e236ed186cd804faefba23e4990978b", size = 174217, upload-time = "2026-04-09T10:20:33.525Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/86/16815fddf056ca998853c6dc525397edf0b43559bb4073a80d2bc7fe8009/msal-1.35.1-py3-none-any.whl", hash = "sha256:8f4e82f34b10c19e326ec69f44dc6b30171f2f7098f3720ea8a9f0c11832caa3", size = 119909, upload-time = "2026-03-04T23:38:50.452Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/414d1f0a5f6f4fe5313c2b002c54e78a3332970feb3f5fed14237aa17064/msal-1.36.0-py3-none-any.whl", hash = "sha256:36ecac30e2ff4322d956029aabce3c82301c29f0acb1ad89b94edcabb0e58ec4", size = 121547, upload-time = "2026-04-09T10:20:32.336Z" }, ] [[package]] @@ -927,11 +839,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] [[package]] @@ -954,11 +866,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] @@ -1013,7 +925,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1021,89 +933,93 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -1122,15 +1038,15 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "10.21" +version = "10.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, ] [[package]] @@ -1144,7 +1060,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1153,9 +1069,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -1228,18 +1144,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] -[[package]] -name = "readchar" -version = "4.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/f8/8657b8cbb4ebeabfbdf991ac40eca8a1d1bd012011bd44ad1ed10f5cb494/readchar-4.2.1.tar.gz", hash = "sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb", size = 9685, upload-time = "2024-11-04T18:28:07.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/10/e4b1e0e5b6b6745c8098c275b69bc9d73e9542d5c7da4f137542b499ed44/readchar-4.2.1-py3-none-any.whl", hash = "sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77", size = 9350, upload-time = "2024-11-04T18:28:02.859Z" }, -] - [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1247,9 +1154,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -1279,52 +1186,40 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "ruff" -version = "0.15.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, - { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, - { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, - { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, - { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, - { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, -] - -[[package]] -name = "runs" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "xmod" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/ae/095cb626504733e288a81f871f86b10530b787d77c50193c170daaca0df1/runs-1.3.0.tar.gz", hash = "sha256:cca304b631dbefec598c7bfbcfb50d6feace6d3a968734b67fd42d3c728f5a05", size = 4585, upload-time = "2026-02-03T15:59:58.974Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/b6/049c75d399ccf6e25abea0652b85bf7e7e101e0300aa9c1d284ad7061c0b/runs-1.3.0-py3-none-any.whl", hash = "sha256:e71a551cfa8da9ef882cac1d5a108bda78c9edee5b8d87e37c1003da5b6a7bed", size = 6406, upload-time = "2026-02-03T15:59:59.96Z" }, +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] @@ -1399,15 +1294,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] -[[package]] -name = "wcwidth" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, -] - [[package]] name = "websocket-client" version = "1.9.0" @@ -1416,12 +1302,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] - -[[package]] -name = "xmod" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/3f/0bc3b89c1dd4dee1f954db4c857f8fbe9cdfa8b25efe370b6d78399a93ac/xmod-1.9.0.tar.gz", hash = "sha256:98b2e7e8e659c51b635f4e98faf3fa1f3f96dab2805f19ddd6e352bbb4d23991", size = 3501, upload-time = "2026-02-03T14:34:48.881Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/a4/74b9510cf2922fb923f6330fd47c049e9e89d984d6dd445c82a85ce7c4e9/xmod-1.9.0-py3-none-any.whl", hash = "sha256:0a549a055e0391a53e356a63552baa7e562560a6e9423c1437cb53b5d4f697a0", size = 4451, upload-time = "2026-02-03T14:34:48.032Z" }, -]