diff --git a/DashAI/alembic/versions/a7d2c9e4f1b0_add_model_name_to_parameter_history.py b/DashAI/alembic/versions/a7d2c9e4f1b0_add_model_name_to_parameter_history.py index ca9604eae..0fbc69370 100644 --- a/DashAI/alembic/versions/a7d2c9e4f1b0_add_model_name_to_parameter_history.py +++ b/DashAI/alembic/versions/a7d2c9e4f1b0_add_model_name_to_parameter_history.py @@ -1,7 +1,7 @@ """Add model_name to parameter_history Revision ID: a7d2c9e4f1b0 -Revises: f1a2b3c4d5e6 +Revises: d4e8a2c6f0b1 Create Date: 2026-07-02 00:00:00.000000 """ @@ -13,7 +13,7 @@ from alembic import op revision: str = "a7d2c9e4f1b0" -down_revision: Union[str, None] = "f1a2b3c4d5e6" +down_revision: Union[str, None] = "d4e8a2c6f0b1" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/DashAI/alembic/versions/d4e8a2c6f0b1_add_credential_table.py b/DashAI/alembic/versions/d4e8a2c6f0b1_add_credential_table.py new file mode 100644 index 000000000..692d87700 --- /dev/null +++ b/DashAI/alembic/versions/d4e8a2c6f0b1_add_credential_table.py @@ -0,0 +1,41 @@ +"""Add credential table + +Revision ID: d4e8a2c6f0b1 +Revises: f1a2b3c4d5e6 +Create Date: 2026-06-15 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "d4e8a2c6f0b1" +down_revision: Union[str, None] = "f1a2b3c4d5e6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "credential", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("encrypted_key", sa.Text(), nullable=False), + sa.Column( + "verified", + sa.Boolean(), + server_default="0", + nullable=False, + ), + sa.Column("created", sa.DateTime(), nullable=False), + sa.Column("last_modified", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id", name="pk_credential"), + sa.UniqueConstraint("name", name="uq_credential_name"), + ) + + +def downgrade() -> None: + op.drop_table("credential") diff --git a/DashAI/back/api/api_v1/api.py b/DashAI/back/api/api_v1/api.py index 1beb875a5..50a11bed7 100644 --- a/DashAI/back/api/api_v1/api.py +++ b/DashAI/back/api/api_v1/api.py @@ -2,6 +2,7 @@ from DashAI.back.api.api_v1.endpoints.components import router as components from DashAI.back.api.api_v1.endpoints.converters import router as converters +from DashAI.back.api.api_v1.endpoints.credentials import router as credentials from DashAI.back.api.api_v1.endpoints.datafile import router as datafile_router from DashAI.back.api.api_v1.endpoints.dataset_source import router as dataset_source from DashAI.back.api.api_v1.endpoints.datasets import router as datasets @@ -46,3 +47,4 @@ api_router_v1.include_router(dataset_source, prefix="/dataset-source") api_router_v1.include_router(datafile_router, prefix="/datafile") api_router_v1.include_router(folders, prefix="/folder") +api_router_v1.include_router(credentials, prefix="/credential") diff --git a/DashAI/back/api/api_v1/endpoints/credentials.py b/DashAI/back/api/api_v1/endpoints/credentials.py new file mode 100644 index 000000000..b99ad1eab --- /dev/null +++ b/DashAI/back/api/api_v1/endpoints/credentials.py @@ -0,0 +1,265 @@ +"""Credential API endpoints.""" + +import logging +from typing import TYPE_CHECKING, Any, Dict, List, Union + +from fastapi import APIRouter, Depends, Header, status +from fastapi.exceptions import HTTPException +from kink import di +from pydantic import BaseModel + +from DashAI.back.credentials.sync import sync_credentials_status + +if TYPE_CHECKING: + from DashAI.back.dependencies.registry import ComponentRegistry + +log = logging.getLogger(__name__) +router = APIRouter() + + +class AuthRequest(BaseModel): + """Request body for authenticating a credential. + + Parameters + ---------- + key : str + The platform key/token to validate and store. + """ + + key: str + + +def _credential_components(registry: "ComponentRegistry") -> Dict[str, Dict[str, Any]]: + """Return the registry's Credential-type components. + + Parameters + ---------- + registry : ComponentRegistry + The component registry. + + Returns + ------- + Dict[str, Dict[str, Any]] + Mapping of credential name to component dict. + """ + return registry._registry.get("Credential", {}) + + +def _localize(value: Any, language: Union[str, None]) -> Union[str, None]: + """Resolve a possibly-multilingual value to a plain string. + + Parameters + ---------- + value : Any + A ``MultilingualString`` or plain value. + language : Union[str, None] + The ``Accept-Language`` header value, or None. + + Returns + ------- + Union[str, None] + The localized string, or the value unchanged when not multilingual. + """ + if hasattr(value, "get"): + lang_code = language.split("-")[0].lower() if language else "en" + return value.get(lang_code) + return value + + +def _status_payload( + name: str, + component_dict: Dict[str, Any], + is_authenticated: bool, + key: Union[str, None], + language: Union[str, None] = None, +) -> Dict[str, Any]: + """Build the status payload for a credential. + + The payload bundles the catalog fields (display name, description) with the + authentication state in a single object so the configuration modal can be + populated with one request. The stored key is included so the modal can + display it, which is acceptable for DashAI's local-first, single-user + desktop model where the database already lives on the user's machine. + + Parameters + ---------- + name : str + Credential component name. + component_dict : Dict[str, Any] + The registry component dict. + is_authenticated : bool + Whether the credential is currently verified. + key : Union[str, None] + The stored decrypted key, or None if nothing is stored. + language : Union[str, None] + The ``Accept-Language`` header used to localize text fields. + + Returns + ------- + Dict[str, Any] + Status payload including localized display name, description and key. + """ + display_name = _localize(component_dict.get("display_name"), language) + description = _localize(component_dict.get("description"), language) + return { + "name": name, + "display_name": display_name or name, + "description": description or "", + "is_authenticated": is_authenticated, + "key": key, + } + + +@router.get("/") +async def list_credentials( + accept_language: Union[str, None] = Header(default=None), + registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]), +) -> List[Dict[str, Any]]: + """List all credential components with their authentication status. + + Returns catalog metadata and auth state together in a single response so + the configuration modal does not need one request per credential. + + Parameters + ---------- + accept_language : Union[str, None] + The 'Accept-Language' header used to localize text fields. + registry : ComponentRegistry + Injected component registry. + + Returns + ------- + list[dict] + Credential status payloads. + """ + creds = _credential_components(registry) + store = di["credential_store"] + statuses = store.all_statuses() + return [ + _status_payload( + name, cdict, statuses.get(name, False), store.load(name), accept_language + ) + for name, cdict in creds.items() + ] + + +@router.get("/{name}") +async def get_credential_status( + name: str, + accept_language: Union[str, None] = Header(default=None), + registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]), +) -> Dict[str, Any]: + """Return the status of a single credential. + + Parameters + ---------- + name : str + Credential component name. + accept_language : Union[str, None] + The 'Accept-Language' header used to localize text fields. + registry : ComponentRegistry + Injected component registry. + + Returns + ------- + dict + Status payload. + + Raises + ------ + HTTPException + 404 if the credential is not registered. + """ + creds = _credential_components(registry) + if name not in creds: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Credential '{name}' not found.", + ) + store = di["credential_store"] + return _status_payload( + name, creds[name], store.is_verified(name), store.load(name), accept_language + ) + + +@router.post("/{name}/auth") +async def authenticate_credential( + name: str, + body: AuthRequest, + registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]), +) -> Dict[str, Any]: + """Verify and store a credential key. + + Parameters + ---------- + name : str + Credential component name. + body : AuthRequest + Contains the key to authenticate with. + registry : ComponentRegistry + Injected component registry. + + Returns + ------- + dict + ``{"is_authenticated": True}`` on success. + + Raises + ------ + HTTPException + 404 if the credential is unknown, 400 if the key is invalid. + """ + creds = _credential_components(registry) + if name not in creds: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Credential '{name}' not found.", + ) + credential = creds[name]["class"]() + try: + credential.auth(body.key) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid credential key.", + ) from exc + + affected = registry.get_required_credentials(name) + sync_credentials_status(only=affected) + return {"is_authenticated": True} + + +@router.delete("/{name}") +async def delete_credential( + name: str, + registry: "ComponentRegistry" = Depends(lambda: di["component_registry"]), +) -> Dict[str, Any]: + """Remove a stored credential key. + + Parameters + ---------- + name : str + Credential component name. + registry : ComponentRegistry + Injected component registry. + + Returns + ------- + dict + ``{"is_authenticated": False}``. + + Raises + ------ + HTTPException + 404 if the credential is not registered. + """ + creds = _credential_components(registry) + if name not in creds: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Credential '{name}' not found.", + ) + di["credential_store"].delete(name) + affected = registry.get_required_credentials(name) + sync_credentials_status(only=affected) + return {"is_authenticated": False} diff --git a/DashAI/back/api/api_v1/endpoints/plugins.py b/DashAI/back/api/api_v1/endpoints/plugins.py index e8cb1cd67..0f35d0783 100644 --- a/DashAI/back/api/api_v1/endpoints/plugins.py +++ b/DashAI/back/api/api_v1/endpoints/plugins.py @@ -243,6 +243,7 @@ async def update_plugin( Plugin The updated plugin. """ + from DashAI.back.credentials.sync import sync_credentials_status from DashAI.back.plugins.utils import ( install_plugin, register_plugin_components, @@ -278,6 +279,7 @@ async def update_plugin( # else the new components should be registered else: register_plugin_components(installed_components, component_registry) + sync_credentials_status() job_queue.put(SyncComponentsJob()) elif ( plugin.status == PluginStatus.INSTALLED diff --git a/DashAI/back/config.py b/DashAI/back/config.py index 6f3a0d507..76d13a261 100644 --- a/DashAI/back/config.py +++ b/DashAI/back/config.py @@ -22,4 +22,5 @@ class DefaultSettings(BaseSettings): EXPLANATIONS_PATH: str = "explanations" NOTEBOOK_PATH: str = "notebook" DATAFILE_PATH: str = "datafiles" + CREDENTIALS_KEY_PATH: str = ".credentials_key" COMPONENT_PATH: str = "components" diff --git a/DashAI/back/config_object.py b/DashAI/back/config_object.py index b994e8e93..8ec29fcee 100644 --- a/DashAI/back/config_object.py +++ b/DashAI/back/config_object.py @@ -1,3 +1,5 @@ +from kink import di + from DashAI.back.core.schema_fields.base_schema import ( BaseSchema, replace_defs_in_schema, @@ -49,3 +51,24 @@ def validate_and_transform(self, raw_data: dict) -> dict: """ schema_instance = self.SCHEMA.model_validate(raw_data) return fill_objects(schema_instance) + + def get_credential(self, name: str): + """Resolve a registered credential component by name. + + The returned instance exposes ``get_key``, ``is_authenticated`` and + ``apply``. When nothing is stored, ``get_key`` returns None and + ``apply`` is a no-op, so optional credentials degrade gracefully. + + Parameters + ---------- + name : str + Credential component class name (e.g. "HuggingFaceCredential"). + + Returns + ------- + BaseCredential + An instance of the requested credential component. + """ + registry = di["component_registry"] + credential_class = registry[name]["class"] + return credential_class() diff --git a/DashAI/back/container.py b/DashAI/back/container.py index 23a4274ce..49b8d1a27 100644 --- a/DashAI/back/container.py +++ b/DashAI/back/container.py @@ -1,8 +1,12 @@ import logging +import os +import pathlib from typing import Dict from kink import Container, di +from DashAI.back.credentials.encryptor import CredentialEncryptor, load_or_create_key +from DashAI.back.credentials.store import CredentialStore from DashAI.back.dependencies.database import setup_sqlite_db from DashAI.back.dependencies.job_queues.huey_job_queue import HueyJobQueue from DashAI.back.dependencies.registry import ComponentRegistry @@ -38,6 +42,13 @@ def build_container(config: Dict[str, str]) -> Container: di["component_registry"] = ComponentRegistry( initial_components=config["INITIAL_COMPONENTS"] ) + credentials_key = load_or_create_key( + pathlib.Path(config["CREDENTIALS_KEY_PATH"]), + env_value=os.getenv("DASHAI_CREDENTIALS_SECRET"), + ) + encryptor = CredentialEncryptor(credentials_key) + di["credential_encryptor"] = encryptor + di["credential_store"] = CredentialStore(session_factory, encryptor) job_queue = HueyJobQueue("job_queue", path_db=config["LOCAL_PATH"]) di["job_queue"] = job_queue diff --git a/DashAI/back/credentials/__init__.py b/DashAI/back/credentials/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/DashAI/back/credentials/base_credential.py b/DashAI/back/credentials/base_credential.py new file mode 100644 index 000000000..0f1574030 --- /dev/null +++ b/DashAI/back/credentials/base_credential.py @@ -0,0 +1,96 @@ +"""Base class for DashAI platform credentials.""" + +from abc import ABC, abstractmethod +from typing import Final, Union + +from kink import di + +from DashAI.back.config_object import ConfigObject +from DashAI.back.core.utils import MultilingualString + + +class BaseCredential(ConfigObject, ABC): + """Abstract base class for all DashAI credentials. + + A credential authenticates against an external platform with a key and + persists it (encrypted) so components that declare it in + ``REQUIRED_CREDENTIALS`` or ``OPTIONAL_CREDENTIALS`` can use it. + + Subclasses only implement :meth:`verify` (the platform-specific network + check) and optionally :meth:`apply` (push the key into the platform SDK). + """ + + TYPE: Final[str] = "Credential" + DISPLAY_NAME: Union[str, MultilingualString] = "" + DESCRIPTION: Union[str, MultilingualString] = "" + ICON: str = "Key" + + @abstractmethod + def verify(self, key: str) -> bool: + """Check a key against the platform. + + Parameters + ---------- + key : str + The key to validate. + + Returns + ------- + bool + True if the key is valid. + """ + raise NotImplementedError + + def auth(self, key: str) -> bool: + """Validate and persist a key. + + Parameters + ---------- + key : str + The key to authenticate with. + + Returns + ------- + bool + True on success. + + Raises + ------ + ValueError + If the key fails verification. + """ + if not self.verify(key): + raise ValueError( + f"Invalid credential for {type(self).__name__}: verification failed." + ) + di["credential_store"].save(type(self).__name__, key) + return True + + def get_key(self) -> Union[str, None]: + """Return the stored decrypted key, or None. + + Returns + ------- + Union[str, None] + The decrypted key, or None if not authenticated. + """ + return di["credential_store"].load(type(self).__name__) + + def is_authenticated(self) -> bool: + """Return whether a verified key is stored. + + Returns + ------- + bool + True if authenticated. + """ + return di["credential_store"].is_verified(type(self).__name__) + + def apply(self) -> None: + """Push the stored key into the platform SDK if present. + + The default implementation is a no-op, which makes a credential safe to + ``apply()`` even when unauthenticated (used by ``OPTIONAL_CREDENTIALS``). + Override in subclasses that need to log in to an SDK. + """ + return None diff --git a/DashAI/back/credentials/encryptor.py b/DashAI/back/credentials/encryptor.py new file mode 100644 index 000000000..1efd4fed2 --- /dev/null +++ b/DashAI/back/credentials/encryptor.py @@ -0,0 +1,102 @@ +"""Symmetric encryption for stored credential keys.""" + +import logging +import os +import stat +from pathlib import Path +from typing import Union + +from cryptography.fernet import Fernet + +logger = logging.getLogger(__name__) + + +def load_or_create_key( + key_path: Path, + env_value: Union[str, None] = None, + persist: bool = True, +) -> bytes: + """Resolve the Fernet secret key. + + Resolution order: explicit ``env_value`` first, then an existing file at + ``key_path``, otherwise a freshly generated key (persisted to ``key_path`` + when ``persist`` is True). + + Parameters + ---------- + key_path : Path + Location of the on-disk key file. + env_value : Union[str, None] + Key provided via environment variable, if any. + persist : bool + Whether to write a newly generated key to disk, by default True. + + Returns + ------- + bytes + The Fernet key as bytes. + """ + if env_value: + return env_value.encode() + + if key_path.exists(): + return key_path.read_bytes() + + key = Fernet.generate_key() + if persist: + key_path.parent.mkdir(parents=True, exist_ok=True) + key_path.write_bytes(key) + try: + os.chmod(key_path, stat.S_IRUSR | stat.S_IWUSR) + except OSError: + logger.warning("Could not restrict permissions on %s", key_path) + return key + + +class CredentialEncryptor: + """Encrypts and decrypts credential keys with Fernet.""" + + def __init__(self, key: bytes) -> None: + """Initialize the encryptor. + + Parameters + ---------- + key : bytes + A valid Fernet key. + """ + self._fernet = Fernet(key) + + def encrypt(self, plaintext: str) -> str: + """Encrypt a plaintext secret. + + Parameters + ---------- + plaintext : str + The secret to encrypt. + + Returns + ------- + str + The encrypted token. + """ + return self._fernet.encrypt(plaintext.encode()).decode() + + def decrypt(self, token: str) -> str: + """Decrypt a token produced by :meth:`encrypt`. + + Parameters + ---------- + token : str + The encrypted token. + + Returns + ------- + str + The decrypted plaintext. + + Raises + ------ + cryptography.fernet.InvalidToken + If the token is invalid or was encrypted with a different key. + """ + return self._fernet.decrypt(token.encode()).decode() diff --git a/DashAI/back/credentials/github_credential.py b/DashAI/back/credentials/github_credential.py new file mode 100644 index 000000000..eb18eafc2 --- /dev/null +++ b/DashAI/back/credentials/github_credential.py @@ -0,0 +1,55 @@ +"""GitHub credential.""" + +import logging +from typing import Final + +from DashAI.back.core.utils import MultilingualString +from DashAI.back.credentials.base_credential import BaseCredential + +logger = logging.getLogger(__name__) + + +class GithubCredential(BaseCredential): + """Credential for the GitHub API.""" + + DISPLAY_NAME: Final = MultilingualString( + en="GitHub", + es="GitHub", + pt="GitHub", + de="GitHub", + zh="GitHub", + ) + DESCRIPTION: Final = MultilingualString( + en="Personal access token for the GitHub API.", + es="Token de acceso personal para la API de GitHub.", + pt="Token de acesso pessoal para a API do GitHub.", + de="Persönliches Zugriffstoken für die GitHub API.", + zh="用于 GitHub API 的个人访问令牌。", + ) + ICON: str = "Key" + + def verify(self, key: str) -> bool: + """Validate a GitHub token via the ``/user`` endpoint. + + Parameters + ---------- + key : str + GitHub personal access token. + + Returns + ------- + bool + True if the token is valid. + """ + import requests + + try: + response = requests.get( + "https://api.github.com/user", + headers={"Authorization": f"Bearer {key}"}, + timeout=10, + ) + return response.status_code == 200 + except Exception as exc: + logger.info("GitHub credential verification failed: %s", exc) + return False diff --git a/DashAI/back/credentials/huggingface_credential.py b/DashAI/back/credentials/huggingface_credential.py new file mode 100644 index 000000000..e3aea0015 --- /dev/null +++ b/DashAI/back/credentials/huggingface_credential.py @@ -0,0 +1,65 @@ +"""HuggingFace Hub credential.""" + +import logging +from typing import Final + +from DashAI.back.core.utils import MultilingualString +from DashAI.back.credentials.base_credential import BaseCredential + +logger = logging.getLogger(__name__) + + +class HuggingFaceCredential(BaseCredential): + """Credential for the HuggingFace Hub.""" + + DISPLAY_NAME: Final = MultilingualString( + en="HuggingFace", + es="HuggingFace", + pt="HuggingFace", + de="HuggingFace", + zh="HuggingFace", + ) + DESCRIPTION: Final = MultilingualString( + en="Access token for the HuggingFace Hub. Required for gated models and " + "datasets.", + es="Token de acceso para el HuggingFace Hub. Necesario para modelos y " + "conjuntos de datos restringidos.", + pt="Token de acesso para o HuggingFace Hub. Necessário para modelos e " + "conjuntos de dados restritos.", + de="Zugriffstoken für den HuggingFace Hub. Erforderlich für " + "eingeschränkte Modelle und Datensätze.", + zh="用于 HuggingFace Hub 的访问令牌。访问受限模型和数据集时需要。", + ) + ICON: str = "Key" + + def verify(self, key: str) -> bool: + """Validate a HuggingFace token via ``whoami``. + + Parameters + ---------- + key : str + HuggingFace access token. + + Returns + ------- + bool + True if the token is valid. + """ + from huggingface_hub import HfApi + + try: + HfApi().whoami(token=key) + return True + except Exception as exc: + logger.info("HuggingFace credential verification failed: %s", exc) + return False + + def apply(self) -> None: + """Log in to the HuggingFace Hub if a key is stored.""" + key = self.get_key() + if not key: + return None + from huggingface_hub import login + + login(token=key) + return None diff --git a/DashAI/back/credentials/kaggle_credential.py b/DashAI/back/credentials/kaggle_credential.py new file mode 100644 index 000000000..b1e6bca34 --- /dev/null +++ b/DashAI/back/credentials/kaggle_credential.py @@ -0,0 +1,108 @@ +"""Kaggle credential.""" + +import logging +import os +from typing import Final + +from DashAI.back.core.utils import MultilingualString +from DashAI.back.credentials.base_credential import BaseCredential + +logger = logging.getLogger(__name__) + + +class KaggleCredential(BaseCredential): + """Credential for the Kaggle API. + + The key is expected in the form ``"username:api_key"``. + """ + + DISPLAY_NAME: Final = MultilingualString( + en="Kaggle", + es="Kaggle", + pt="Kaggle", + de="Kaggle", + zh="Kaggle", + ) + DESCRIPTION: Final = MultilingualString( + en="Kaggle API credential in the form 'username:key'.", + es="Credencial de la API de Kaggle en el formato 'usuario:clave'.", + pt="Credencial da API do Kaggle no formato 'usuario:chave'.", + de="Zugangsdaten für die Kaggle API im Format 'benutzername:schluessel'.", + zh="Kaggle API 凭证,格式为 'username:key'。", + ) + ICON: str = "Key" + + @staticmethod + def _split_key(key: str): + """Split a ``"username:api_key"`` credential into its parts. + + Parameters + ---------- + key : str + Kaggle credential in the form ``"username:api_key"``. + + Returns + ------- + tuple[str, str] or None + ``(username, api_key)`` if well formed, otherwise None. + """ + username, separator, api_key = key.partition(":") + if not separator or not username or not api_key: + return None + return username, api_key + + def verify(self, key: str) -> bool: + """Validate a Kaggle credential with the official ``kaggle`` library. + + The credentials are exported to the environment before importing + ``kaggle``, because the package authenticates at import time and + terminates the process when no credentials are available. + + Parameters + ---------- + key : str + Kaggle credential in the form ``"username:api_key"``. + + Returns + ------- + bool + True if the credential authenticates successfully. + """ + parts = self._split_key(key) + if parts is None: + return False + username, api_key = parts + + os.environ["KAGGLE_USERNAME"] = username + os.environ["KAGGLE_KEY"] = api_key + try: + from kaggle.api.kaggle_api_extended import KaggleApi + + api = KaggleApi() + api.authenticate() + # Perform an authenticated call to confirm the key is valid. + api.competitions_list() + return True + except SystemExit: + return False + except Exception as exc: + logger.info("Kaggle credential verification failed: %s", exc) + return False + + def apply(self) -> None: + """Export the stored Kaggle credentials to the environment. + + The official ``kaggle`` library reads ``KAGGLE_USERNAME`` and + ``KAGGLE_KEY`` from the environment, so exporting them makes any later + use of the library authenticated. No-op when nothing is stored. + """ + key = self.get_key() + if not key: + return None + parts = self._split_key(key) + if parts is None: + return None + username, api_key = parts + os.environ["KAGGLE_USERNAME"] = username + os.environ["KAGGLE_KEY"] = api_key + return None diff --git a/DashAI/back/credentials/store.py b/DashAI/back/credentials/store.py new file mode 100644 index 000000000..1a3047f5a --- /dev/null +++ b/DashAI/back/credentials/store.py @@ -0,0 +1,115 @@ +"""Persistence boundary for encrypted credentials.""" + +import logging +from datetime import datetime +from typing import Dict, Union + +from DashAI.back.credentials.encryptor import CredentialEncryptor +from DashAI.back.dependencies.database.models import Credential + +logger = logging.getLogger(__name__) + + +class CredentialStore: + """Reads and writes encrypted credentials in the database. + + This is the only component that touches the credential table and the + encryptor. + """ + + def __init__(self, session_factory, encryptor: CredentialEncryptor) -> None: + """Initialize the store. + + Parameters + ---------- + session_factory + SQLAlchemy session factory (callable returning a session). + encryptor : CredentialEncryptor + Encryptor used to protect keys at rest. + """ + self._session_factory = session_factory + self._encryptor = encryptor + + def save(self, name: str, key: str) -> None: + """Encrypt and persist a credential key, marking it verified. + + Parameters + ---------- + name : str + Credential component name. + key : str + Plaintext key to store. + """ + encrypted = self._encryptor.encrypt(key) + with self._session_factory() as db: + row = db.query(Credential).filter_by(name=name).first() + if row is None: + row = Credential(name=name, encrypted_key=encrypted, verified=True) + db.add(row) + else: + row.encrypted_key = encrypted + row.verified = True + row.last_modified = datetime.now() + db.commit() + + def load(self, name: str) -> Union[str, None]: + """Return the decrypted key for a credential, or None. + + Parameters + ---------- + name : str + Credential component name. + + Returns + ------- + Union[str, None] + Decrypted key, or None if not stored. + """ + with self._session_factory() as db: + row = db.query(Credential).filter_by(name=name).first() + if row is None: + return None + return self._encryptor.decrypt(row.encrypted_key) + + def is_verified(self, name: str) -> bool: + """Return whether a credential is stored and verified. + + Parameters + ---------- + name : str + Credential component name. + + Returns + ------- + bool + True if a verified key exists. + """ + with self._session_factory() as db: + row = db.query(Credential).filter_by(name=name).first() + return bool(row and row.verified) + + def delete(self, name: str) -> None: + """Remove a stored credential. + + Parameters + ---------- + name : str + Credential component name. + """ + with self._session_factory() as db: + row = db.query(Credential).filter_by(name=name).first() + if row is not None: + db.delete(row) + db.commit() + + def all_statuses(self) -> Dict[str, bool]: + """Return the verified status of every stored credential. + + Returns + ------- + Dict[str, bool] + Mapping of credential name to verified status. + """ + with self._session_factory() as db: + rows = db.query(Credential).all() + return {row.name: bool(row.verified) for row in rows} diff --git a/DashAI/back/credentials/sync.py b/DashAI/back/credentials/sync.py new file mode 100644 index 000000000..27fbce8f2 --- /dev/null +++ b/DashAI/back/credentials/sync.py @@ -0,0 +1,22 @@ +"""Synchronize credential availability flags on the component registry.""" + +import logging +from typing import List, Union + +from kink import di + +logger = logging.getLogger(__name__) + + +def sync_credentials_status(only: Union[List[str], None] = None) -> None: + """Refresh ``credentials_satisfied`` flags from stored credential statuses. + + Parameters + ---------- + only : Union[List[str], None] + If provided, only these component names are recomputed. If None, all + components are recomputed. + """ + store = di["credential_store"] + registry = di["component_registry"] + registry.refresh_credentials_status(store.all_statuses(), only=only) diff --git a/DashAI/back/dataset_sources/huggingface_dataset_source.py b/DashAI/back/dataset_sources/huggingface_dataset_source.py index 3776cf57e..73a7725a6 100644 --- a/DashAI/back/dataset_sources/huggingface_dataset_source.py +++ b/DashAI/back/dataset_sources/huggingface_dataset_source.py @@ -23,6 +23,8 @@ class HuggingFaceDatasetSource(BaseDatasetSource): offset and slicing the iterator. """ + OPTIONAL_CREDENTIALS = ["HuggingFaceCredential"] + DISPLAY_NAME: Final = MultilingualString( en="HuggingFace Hub", es="HuggingFace Hub", @@ -190,6 +192,10 @@ def download_dataset(self, dataset_id: str, temp_path: str) -> str: """ from huggingface_hub import snapshot_download + # Apply the HuggingFace credential if one is stored; this is a no-op for + # public datasets and only matters for gated/private ones. + self.get_credential("HuggingFaceCredential").apply() + snapshot_download( repo_id=dataset_id, repo_type="dataset", diff --git a/DashAI/back/dependencies/config_builder.py b/DashAI/back/dependencies/config_builder.py index 2966bc203..540c50152 100644 --- a/DashAI/back/dependencies/config_builder.py +++ b/DashAI/back/dependencies/config_builder.py @@ -59,6 +59,7 @@ def build_config_dict( config["RUNS_PATH"] = local_path / config["RUNS_PATH"] config["IMAGES_PATH"] = local_path / config["IMAGES_PATH"] config["DATAFILE_PATH"] = local_path / config["DATAFILE_PATH"] + config["CREDENTIALS_KEY_PATH"] = local_path / config["CREDENTIALS_KEY_PATH"] config["COMPONENT_PATH"] = local_path / config["COMPONENT_PATH"] config["FRONT_BUILD_PATH"] = pathlib.Path(config["FRONT_BUILD_PATH"]).absolute() config["BACK_PATH"] = pathlib.Path(config["BACK_PATH"]).absolute() diff --git a/DashAI/back/dependencies/database/models.py b/DashAI/back/dependencies/database/models.py index 20b547eb6..97d7894d4 100644 --- a/DashAI/back/dependencies/database/models.py +++ b/DashAI/back/dependencies/database/models.py @@ -782,3 +782,22 @@ class Datafile(Base): name="uq_datafile_source_dataset", ), ) + + +class Credential(Base): + __tablename__ = "credential" + """ + Table to store encrypted credentials for external platforms. + """ + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String, unique=True, nullable=False) + encrypted_key: Mapped[str] = mapped_column(Text, nullable=False) + verified: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=False, server_default="0" + ) + created: Mapped[DateTime] = mapped_column(DateTime, default=datetime.now) + last_modified: Mapped[DateTime] = mapped_column( + DateTime, + default=datetime.now, + onupdate=datetime.now, + ) diff --git a/DashAI/back/dependencies/registry/component_registry.py b/DashAI/back/dependencies/registry/component_registry.py index c6f55603d..7d68b7ce8 100644 --- a/DashAI/back/dependencies/registry/component_registry.py +++ b/DashAI/back/dependencies/registry/component_registry.py @@ -212,6 +212,13 @@ def register_component(self, new_component: Type) -> None: if isinstance(display_name, str): new_component.DISPLAY_NAME = MultilingualString(en=display_name) + required_credentials = list( + getattr(new_component, "REQUIRED_CREDENTIALS", []) or [] + ) + optional_credentials = list( + getattr(new_component, "OPTIONAL_CREDENTIALS", []) or [] + ) + new_register_component = { "name": new_component.__name__, "type": base_type, @@ -222,6 +229,9 @@ def register_component(self, new_component: Type) -> None: "description": getattr(new_component, "DESCRIPTION", None), "display_name": getattr(new_component, "DISPLAY_NAME", None), "color": getattr(new_component, "COLOR", None), + "required_credentials": required_credentials, + "optional_credentials": optional_credentials, + "credentials_satisfied": len(required_credentials) == 0, } if base_type not in self._registry: @@ -236,8 +246,19 @@ def register_component(self, new_component: Type) -> None: self._relationship_manager.add_relationship( new_component.__name__, compatible_component, + "compatible_components", ) + for credential_name in required_credentials: + self._relationship_manager.add_relationship( + new_component.__name__, credential_name, "required_credentials" + ) + + for credential_name in optional_credentials: + self._relationship_manager.add_relationship( + new_component.__name__, credential_name, "optional_credentials" + ) + def seed_download_status(self) -> None: """Populate the ``downloaded`` flag for every registered component. @@ -321,8 +342,19 @@ def unregister_component(self, component: Type) -> None: self._relationship_manager.remove_relationship( component.__name__, compatible_component, + "compatible_components", ) + for credential_name in getattr(component, "REQUIRED_CREDENTIALS", []) or []: + self._relationship_manager.remove_relationship( + component.__name__, credential_name, "required_credentials" + ) + + for credential_name in getattr(component, "OPTIONAL_CREDENTIALS", []) or []: + self._relationship_manager.remove_relationship( + component.__name__, credential_name, "optional_credentials" + ) + @beartype def get_components_by_types( self, @@ -515,5 +547,68 @@ def get_related_components(self, component_id: str) -> List[Dict[str, Any]]: return [ self.__getitem__(related_component_id) - for related_component_id in self._relationship_manager[component_id] + for related_component_id in self._relationship_manager.get( + component_id, "compatible_components" + ) ] + + @beartype + def get_required_credentials(self, component_id: str) -> List[str]: + """Return the names of credentials a component requires. + + Parameters + ---------- + component_id : str + A registered component name. + + Returns + ------- + List[str] + Names of required credential components (empty if none). + """ + return self._relationship_manager.get(component_id, "required_credentials") + + @beartype + def get_optional_credentials(self, component_id: str) -> List[str]: + """Return the names of credentials a component can optionally use. + + Parameters + ---------- + component_id : str + A registered component name. + + Returns + ------- + List[str] + Names of optional credential components (empty if none). + """ + return self._relationship_manager.get(component_id, "optional_credentials") + + @beartype + def refresh_credentials_status( + self, + statuses: Dict[str, bool], + only: Union[List[str], None] = None, + ) -> None: + """Recompute the ``credentials_satisfied`` flag of components. + + A component is satisfied when every credential in its + ``required_credentials`` is verified. Components with no required + credentials are always satisfied. + + Parameters + ---------- + statuses : Dict[str, bool] + Mapping of credential component name to verified status. + only : Union[List[str], None] + If provided, only these component names are recomputed. If None, + all components are recomputed. + """ + for type_registry in self._registry.values(): + for name, component_dict in type_registry.items(): + if only is not None and name not in only: + continue + required = component_dict.get("required_credentials", []) + component_dict["credentials_satisfied"] = all( + statuses.get(credential_name, False) for credential_name in required + ) diff --git a/DashAI/back/dependencies/registry/relationship_manager.py b/DashAI/back/dependencies/registry/relationship_manager.py index 03f21d8e6..d024c7428 100644 --- a/DashAI/back/dependencies/registry/relationship_manager.py +++ b/DashAI/back/dependencies/registry/relationship_manager.py @@ -8,37 +8,31 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +DEFAULT_RELATION_TYPE = "compatible_components" + class RelationshipManager: - """Class that implements a relationship registry between DashAI components. - - The registry is a pair of dicts (defaultdicts) that stores the relationships as a - dictionary where its keys are some class and its values a list of classes that are - related with the class. - - For example, a `_relation`that stores relations between - "TabularClassificationTask" and "SVM", "KNN" models and "CSVDataloader" loader - could be: - - ``` - { - "TabularClassificationTask": ["SVC", "KNN", "CSVDataloader", ...], - "SVC": ["TabularClassificationTask"], - "KNN": ["TabularClassificationTask"], - "CSVDataloader": ["TabularClassificationTask"], - } - ``` - Note that the relations are duplicated and hopefully, consistent between them. + """Registry of typed relationships between DashAI components. + Relations are stored as a nested mapping + ``{component_id: {relation_type: [related_component_id, ...]}}``. + Each relation is stored bidirectionally and scoped by ``relation_type`` + (for example ``"compatible_components"``, ``"required_credentials"`` or + ``"optional_credentials"``). """ def __init__(self) -> None: """Initialize the relationship manager.""" - self._relations: DefaultDict[str, List[str]] = defaultdict(list) + self._relations: DefaultDict[str, DefaultDict[str, List[str]]] = defaultdict( + lambda: defaultdict(list) + ) @property - def relations(self) -> Dict[str, List[str]]: - return dict(self._relations) + def relations(self) -> Dict[str, Dict[str, List[str]]]: + return { + component_id: dict(relations) + for component_id, relations in self._relations.items() + } @relations.setter def relations(self, _: Any) -> None: @@ -54,11 +48,12 @@ def relations(self, _: Any) -> None: @beartype def add_relationship( - self, first_component_id: str, second_component_id: str + self, + first_component_id: str, + second_component_id: str, + relation_type: str = DEFAULT_RELATION_TYPE, ) -> None: - """Add a new relation to the relationship manager. - - Note that the relation is bidirectional. + """Add a new bidirectional relation of the given type. Parameters ---------- @@ -66,16 +61,20 @@ def add_relationship( First component id or name. second_component_id : str Second component id or name. - + relation_type : str + The relation category, by default ``"compatible_components"``. """ - self._relations[first_component_id].append(second_component_id) - self._relations[second_component_id].append(first_component_id) + self._relations[first_component_id][relation_type].append(second_component_id) + self._relations[second_component_id][relation_type].append(first_component_id) @beartype def remove_relationship( - self, first_component_id: str, second_component_id: str + self, + first_component_id: str, + second_component_id: str, + relation_type: str = DEFAULT_RELATION_TYPE, ) -> None: - """Remove an existing relation to the relationship manager. + """Remove an existing relation of the given type. Parameters ---------- @@ -83,24 +82,26 @@ def remove_relationship( First component id or name. second_component_id : str Second component id or name. + relation_type : str + The relation category, by default ``"compatible_components"``. + Raises + ------ + ValueError + If the relation does not exist. """ try: - self._relations[first_component_id].remove(second_component_id) - except KeyError as e: + self._relations[first_component_id][relation_type].remove( + second_component_id + ) + self._relations[second_component_id][relation_type].remove( + first_component_id + ) + except ValueError as e: raise ValueError( - f"Error: Relationship between {first_component_id} and does " - f"not exist {second_component_id} in the registry. Exception: " - f"{e}" - ) from e - - try: - self._relations[second_component_id].remove(first_component_id) - except KeyError as e: - raise ValueError( - f"Error: Relationship between {second_component_id} and does " - f"not exist {first_component_id} in the registry. Exception: " - f"{e}" + f"Error: Relationship of type '{relation_type}' between " + f"{first_component_id} and {second_component_id} does not exist " + f"in the registry. Exception: {e}" ) from e logger.info( @@ -108,28 +109,47 @@ def remove_relationship( f"{first_component_id}, {second_component_id}" ) + @beartype + def get( + self, component_id: str, relation_type: str = DEFAULT_RELATION_TYPE + ) -> List[str]: + """Return the related component ids of a given type. + + Parameters + ---------- + component_id : str + A component name or id. + relation_type : str + The relation category, by default ``"compatible_components"``. + + Returns + ------- + list[str] + Related component ids, or an empty list if none exist. + """ + if component_id in self._relations: + return list(self._relations[component_id].get(relation_type, [])) + return [] + @beartype def __contains__(self, component_id: str) -> bool: - """Indicate if the relation manager contains a relationship. + """Indicate if the relation manager contains a component. Parameters ---------- component_id : str - The id of the component to be checked if a relationship exists or not. + The id of the component to check. Returns ------- bool - True if the relation exists, False otherwise. + True if the component has any relation, False otherwise. """ return component_id in self._relations @beartype - def __getitem__(self, component_id: str) -> List[str]: - """Obtain all stored relationships from a specific component. - - Return an empty list if the component id does not exists in the relationship - manager. + def __getitem__(self, component_id: str) -> Dict[str, List[str]]: + """Obtain all stored relationships for a component, grouped by type. Parameters ---------- @@ -138,10 +158,10 @@ def __getitem__(self, component_id: str) -> List[str]: Returns ------- - list[str] - A list with the related components. + dict[str, list[str]] + Mapping of relation type to related component ids. Empty dict if + the component is unknown. """ if component_id in self._relations: - return self._relations[component_id] - - return [] + return dict(self._relations[component_id]) + return {} diff --git a/DashAI/back/initial_components.py b/DashAI/back/initial_components.py index 019ecca95..da87c0262 100644 --- a/DashAI/back/initial_components.py +++ b/DashAI/back/initial_components.py @@ -61,6 +61,10 @@ from DashAI.back.converters.simple_converters.column_remover import ColumnRemover from DashAI.back.converters.simple_converters.nan_remover import NanRemover +# Credentials +from DashAI.back.credentials.huggingface_credential import HuggingFaceCredential +from DashAI.back.credentials.kaggle_credential import KaggleCredential + # DataLoaders from DashAI.back.dataloaders.classes.arff_dataloader import ARFFDataLoader from DashAI.back.dataloaders.classes.csv_dataloader import CSVDataLoader @@ -450,6 +454,9 @@ def get_initial_components(): HuggingFaceDatasetSource, OpenMLDatasetSource, ZenodoDatasetSource, + # Credentials + HuggingFaceCredential, + KaggleCredential, # Metrics F1, Accuracy, diff --git a/DashAI/back/models/hugging_face/stable_diffusion_v3_model.py b/DashAI/back/models/hugging_face/stable_diffusion_v3_model.py index ca8daef20..922149500 100644 --- a/DashAI/back/models/hugging_face/stable_diffusion_v3_model.py +++ b/DashAI/back/models/hugging_face/stable_diffusion_v3_model.py @@ -21,57 +21,72 @@ class StableDiffusionSchema(BaseSchema): """Configuration schema for Stable Diffusion V3 text-to-image generation. - Configures the checkpoint variant (``model_name``), HuggingFace access key - (``huggingface_key``), prompt conditioning (``negative_prompt``), + Configures the checkpoint variant (``model_name``), + prompt conditioning (``negative_prompt``), denoising schedule (``num_inference_steps``), prompt adherence (``guidance_scale``), output dimensions (``width``, ``height``), reproducibility (``seed``), hardware target (``device``), and batch size (``num_images_per_prompt``) for ``StableDiffusionV3Model``. """ - huggingface_key: schema_field( - string_field(), - placeholder="", + model_name: schema_field( + enum_field( + enum=[ + "stabilityai/stable-diffusion-3-medium-diffusers", + "stabilityai/stable-diffusion-3.5-medium", + "stabilityai/stable-diffusion-3.5-large", + "stabilityai/stable-diffusion-3.5-large-turbo", + ] + ), + placeholder="stabilityai/stable-diffusion-3-medium-diffusers", description=MultilingualString( en=( - "Hugging Face read-access token required to download these gated " - "models. To obtain one: accept the model license on " - "huggingface.co/stabilityai, then go to Settings → Access Tokens " - "and generate a token with 'Read' scope." + "The SD3/SD3.5 checkpoint to load. 'sd-3-medium' is the baseline " + "2B-parameter model. 'sd-3.5-medium' improves quality at similar " + "speed. 'sd-3.5-large' (8B) delivers the highest quality but needs " + "more VRAM. 'sd-3.5-large-turbo' is a distilled large model that " + "requires far fewer steps (4-8) for fast high-quality generation. " + "All variants target 1024x1024 px natively." ), es=( - "Token de acceso de lectura de Hugging Face necesario para descargar " - "estos modelos protegidos. Para obtenerlo: acepte la licencia del " - "modelo en huggingface.co/stabilityai, luego vaya a " - "Configuración → Tokens de Acceso y genere un token con alcance " - "'Read'." + "El checkpoint SD3/SD3.5 a cargar. 'sd-3-medium' es el modelo base " + "de 2B parámetros. 'sd-3.5-medium' mejora la calidad a velocidad " + "similar. 'sd-3.5-large' (8B) ofrece la mayor calidad pero necesita " + "más VRAM. 'sd-3.5-large-turbo' es un modelo large destilado que " + "requiere muchos menos pasos (4-8) para generación rápida de alta " + "calidad. Todas las variantes apuntan a 1024x1024 px de forma nativa." ), pt=( - "Token de acesso de leitura do Hugging Face necessário para baixar " - "esses modelos protegidos. Para obtê-lo: aceite a licença do " - "modelo em huggingface.co/stabilityai, depois vá em " - "Configurações → Tokens de Acesso e gere um token com escopo " - "'Read'." + "O checkpoint SD3/SD3.5 a carregar. 'sd-3-medium' é o modelo base " + "de 2B parâmetros. 'sd-3.5-medium' melhora a qualidade a velocidade " + "similar. 'sd-3.5-large' (8B) oferece a maior qualidade mas precisa " + "de mais VRAM. 'sd-3.5-large-turbo' é um modelo large destilado que " + "requer muito menos passos (4-8) para geração rápida de alta " + "qualidade. " + "Todas as variantes visam 1024x1024 px nativamente." ), de=( - "Hugging Face Lesezugriffs-Token, der zum Herunterladen dieser " - "geschützten Modelle erforderlich ist. So erhalten Sie ihn: Akzeptieren" - "Sie die Modell-Lizenz auf huggingface.co/stabilityai, dann gehen Sie " - "zu Einstellungen → Zugriffstoken und generieren Sie einen Token " - "mit 'Read'-Umfang." + "Der zu ladende SD3/SD3.5-Checkpoint. 'sd-3-medium' ist das " + "2B-Parameter-Basismodell. 'sd-3.5-medium' verbessert die Qualität " + "bei ähnlicher Geschwindigkeit. 'sd-3.5-large' (8B) liefert die höchste" + "Qualität, benötigt aber mehr VRAM. 'sd-3.5-large-turbo' ist ein " + "destilliertes Large-Modell, das deutlich weniger Schritte (4-8) für " + "schnelle hochwertige Generierung benötigt. " + "Alle Varianten zielen nativ auf 1024x1024 px ab." ), zh=( - "下载受限模型所需的 Hugging Face 只读访问令牌。获取方式:在 " - "huggingface.co/stabilityai 接受模型许可证,然后进入" - "设置 → 访问令牌,生成具有 'Read' 权限的令牌。" + "要加载的 SD3/SD3.5 检查点。'sd-3-medium' 是 2B 参数基准模型。" + "'sd-3.5-medium' 以相近速度提升质量。'sd-3.5-large'(8B)质量最高但" + "需要更多显存。'sd-3.5-large-turbo' 是蒸馏版大模型,仅需 4-8 步即可" + "快速生成高质量图像。所有变体原生目标分辨率为 1024x1024 像素。" ), ), alias=MultilingualString( - en="Hugging Face key", - es="Clave Hugging Face", - pt="Chave Hugging Face", - de="Hugging Face-Schlüssel", - zh="Hugging Face 密钥", + en="Model name", + es="Nombre del modelo", + pt="Nome do modelo", + de="Modellname", + zh="模型名称", ), ) # type: ignore @@ -421,6 +436,7 @@ class StableDiffusion3GenerationModel( """ SCHEMA = StableDiffusionSchema + REQUIRED_CREDENTIALS = ["HuggingFaceCredential"] MODEL_NAME: str = "" COLOR: str = "#6a1b9a" DISPLAY_NAME: str = MultilingualString( @@ -482,23 +498,17 @@ def __init__(self, **kwargs): import torch from diffusers import DiffusionPipeline - from huggingface_hub import login kwargs = self.validate_and_transform(kwargs) use_gpu = DEVICE_TO_IDX.get(kwargs.get("device")) >= 0 self.device = ( f"cuda:{DEVICE_TO_IDX.get(kwargs.get('device'))}" if use_gpu else "cpu" ) + # Log in to HuggingFace using the stored credential so the gated + # checkpoints can be downloaded. + self.get_credential("HuggingFaceCredential").apply() + self.model_name = self._pretrained_source(None) - self.huggingface_key = kwargs.get("huggingface_key") - - if self.huggingface_key: - try: - login(token=self.huggingface_key) - except Exception as e: - raise ValueError( - "Failed to login to Hugging Face. Please check your API key." - ) from e try: self.model = DiffusionPipeline.from_pretrained( diff --git a/DashAI/front/src/api/credentials.ts b/DashAI/front/src/api/credentials.ts new file mode 100644 index 000000000..f3e3a43ed --- /dev/null +++ b/DashAI/front/src/api/credentials.ts @@ -0,0 +1,27 @@ +import api from "./api"; +import type { ICredential } from "../types/credential"; + +export const getCredentials = async (): Promise => { + const response = await api.get("/v1/credential/"); + return response.data; +}; + +export const authenticateCredential = async ( + name: string, + key: string, +): Promise<{ is_authenticated: boolean }> => { + const response = await api.post<{ is_authenticated: boolean }>( + `/v1/credential/${name}/auth`, + { key }, + ); + return response.data; +}; + +export const deleteCredential = async ( + name: string, +): Promise<{ is_authenticated: boolean }> => { + const response = await api.delete<{ is_authenticated: boolean }>( + `/v1/credential/${name}`, + ); + return response.data; +}; diff --git a/DashAI/front/src/components/ResponsiveAppBar.jsx b/DashAI/front/src/components/ResponsiveAppBar.jsx index 47a16d296..10c06ec2d 100644 --- a/DashAI/front/src/components/ResponsiveAppBar.jsx +++ b/DashAI/front/src/components/ResponsiveAppBar.jsx @@ -18,6 +18,7 @@ import LightModeOutlinedIcon from "@mui/icons-material/LightModeOutlined"; import Tooltip from "@mui/material/Tooltip"; import HardwareMonitorButton from "./hardware/HardwareMonitorButton"; import NavbarTourButton from "./tour/NavbarTourButton"; +import CredentialsButton from "./credentials/CredentialsButton"; function ResponsiveAppBar() { const theme = useTheme(); @@ -220,6 +221,7 @@ function ResponsiveAppBar() { + () => null); + describe("ResponsiveAppBar", () => { it("renders without crashing", () => { renderWithProviders(); diff --git a/DashAI/front/src/components/credentials/CredentialsButton.jsx b/DashAI/front/src/components/credentials/CredentialsButton.jsx new file mode 100644 index 000000000..048116863 --- /dev/null +++ b/DashAI/front/src/components/credentials/CredentialsButton.jsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; +import VpnKeyOutlinedIcon from "@mui/icons-material/VpnKeyOutlined"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import CredentialsDialog from "./CredentialsDialog"; + +export default function CredentialsButton() { + const theme = useTheme(); + const { t } = useTranslation("credentials"); + const [open, setOpen] = useState(false); + + const iconBtnSx = { + width: 32, + height: 32, + borderRadius: "4px", + border: `1px solid ${theme.palette.divider}`, + color: theme.palette.text.secondary, + "&:hover": { + background: theme.palette.ui.hover, + color: theme.palette.text.primary, + }, + }; + + return ( + <> + + setOpen(true)} + aria-label="credentials" + sx={iconBtnSx} + > + + + + setOpen(false)} /> + + ); +} diff --git a/DashAI/front/src/components/credentials/CredentialsDialog.jsx b/DashAI/front/src/components/credentials/CredentialsDialog.jsx new file mode 100644 index 000000000..e1b864236 --- /dev/null +++ b/DashAI/front/src/components/credentials/CredentialsDialog.jsx @@ -0,0 +1,266 @@ +import React, { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { + Dialog, + DialogTitle, + DialogContent, + Stack, + TextField, + Button, + Typography, + Box, + IconButton, + InputAdornment, + Tooltip, +} from "@mui/material"; +import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined"; +import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import CloseIcon from "@mui/icons-material/Close"; +import VpnKeyOutlinedIcon from "@mui/icons-material/VpnKeyOutlined"; +import { useTranslation } from "react-i18next"; +import { useSnackbar } from "notistack"; +import { + getCredentials, + authenticateCredential, + deleteCredential, +} from "../../api/credentials"; + +function CredentialRow({ credential, onChanged }) { + const { t } = useTranslation("credentials"); + const { enqueueSnackbar } = useSnackbar(); + const [key, setKey] = useState(credential.key ?? ""); + const [showKey, setShowKey] = useState(false); + const [busy, setBusy] = useState(false); + + const authed = credential.is_authenticated; + + const handleVerify = async () => { + setBusy(true); + try { + await authenticateCredential(credential.name, key); + enqueueSnackbar(t("verifySuccess"), { variant: "success" }); + onChanged(); + } catch { + enqueueSnackbar(t("verifyError"), { variant: "error" }); + } finally { + setBusy(false); + } + }; + + const handleRemove = async () => { + setBusy(true); + try { + await deleteCredential(credential.name); + setKey(""); + onChanged(); + } finally { + setBusy(false); + } + }; + + return ( + ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: 2, + p: 2, + transition: "border-color 0.15s, background 0.15s", + "&:hover": { borderColor: theme.palette.text.disabled }, + })} + > + {/* Identity + status */} + + + + {credential.display_name} + + {credential.description && ( + + {credential.description} + + )} + + + ({ + width: 8, + height: 8, + borderRadius: "50%", + backgroundColor: authed + ? theme.palette.success.main + : theme.palette.text.disabled, + })} + /> + ({ + color: authed + ? theme.palette.success.main + : theme.palette.text.secondary, + fontWeight: 500, + })} + > + {authed ? t("authenticated") : t("notAuthenticated")} + + + + + {/* Key input + actions */} + + setKey(e.target.value)} + sx={{ "& input": { fontFamily: "monospace", fontSize: 13 } }} + InputProps={{ + endAdornment: ( + + setShowKey((prev) => !prev)} + edge="end" + > + {showKey ? ( + + ) : ( + + )} + + + ), + }} + /> + + {authed && ( + + + + + + + + )} + + + ); +} + +CredentialRow.propTypes = { + credential: PropTypes.object.isRequired, + onChanged: PropTypes.func.isRequired, +}; + +export default function CredentialsDialog({ open, onClose }) { + const { t } = useTranslation("credentials"); + const [credentials, setCredentials] = useState([]); + + const refresh = async () => { + try { + const data = await getCredentials(); + setCredentials(Array.isArray(data) ? data : []); + } catch { + // silently ignore fetch errors (e.g. in test environments) + } + }; + + useEffect(() => { + if (open) { + refresh(); + } + }, [open]); + + return ( + + + + ({ + width: 36, + height: 36, + borderRadius: 1.5, + flexShrink: 0, + display: "flex", + alignItems: "center", + justifyContent: "center", + color: theme.palette.primary.main, + backgroundColor: theme.palette.ui.hover, + })} + > + + + + + {t("title")} + + + {t("subtitle")} + + + + + + + + + + {credentials.map((credential) => ( + + ))} + + + + ); +} + +CredentialsDialog.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, +}; diff --git a/DashAI/front/src/components/custom/ComponentSelector.jsx b/DashAI/front/src/components/custom/ComponentSelector.jsx index c2a399dec..2028fecbd 100644 --- a/DashAI/front/src/components/custom/ComponentSelector.jsx +++ b/DashAI/front/src/components/custom/ComponentSelector.jsx @@ -10,12 +10,15 @@ import { Typography, Collapse, Paper, + Tooltip, } from "@mui/material"; import { Search as SearchIcon, Clear as ClearIcon, ExpandMore as ExpandMoreIcon, Check as CheckIcon, + LockOutlined as LockIcon, + VpnKeyOutlined as KeyIcon, } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import ComponentDownloadControl from "../models/model/ComponentDownloadControl"; @@ -31,6 +34,13 @@ function getDescription(component, fallback = "") { return component.description ?? fallback; } +// Derive a human label from a credential component name, e.g. +// "HuggingFaceCredential" -> "HuggingFace". Falls back to the raw name so it +// works for any backend-registered credential without frontend changes. +function credentialLabel(name) { + return name.replace(/Credential$/, "") || name; +} + function ComponentSelector({ components, selected = null, @@ -44,7 +54,7 @@ function ComponentSelector({ tourDataMatchFn = null, onDownloadChange = null, }) { - const { t } = useTranslation(["custom", "common"]); + const { t } = useTranslation(["custom", "credentials", "common"]); const [search, setSearch] = useState(""); const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY); @@ -109,8 +119,22 @@ function ComponentSelector({ const renderCard = (component) => { const isSelected = selected?.name === component.name; const icon = getIcon?.(component); + const requiredCredentials = component.required_credentials ?? []; + const optionalCredentials = component.optional_credentials ?? []; + const credentialsSatisfied = component.credentials_satisfied !== false; + // Unmet required credentials make the component unusable: lock and disable. + const locked = !credentialsSatisfied && requiredCredentials.length > 0; + const requiredPlatforms = requiredCredentials + .map(credentialLabel) + .join(", "); + const optionalPlatforms = optionalCredentials + .map(credentialLabel) + .join(", "); const requiresDownload = Boolean(component.metadata?.requires_download); const needsDownload = requiresDownload && !component.downloaded; + // Component is usable only when its download is present AND its required + // credentials are satisfied; either gap disables selection. + const disabled = locked || needsDownload; const isCsvComponent = tourDataFor && (tourDataMatchFn @@ -120,32 +144,33 @@ function ComponentSelector({ handleSelect(component)} + onClick={() => { + if (!disabled) handleSelect(component); + }} + aria-disabled={disabled} data-tour={isCsvComponent ? tourDataFor : undefined} sx={{ p: 3, display: "flex", flexDirection: "column", gap: 3, - cursor: "pointer", + cursor: disabled ? "not-allowed" : "pointer", border: 1, borderColor: isSelected ? "primary.main" : "divider", bgcolor: isSelected ? "action.selected" : "background.paper", transition: "border-color 0.15s, background 0.15s", - "&:hover": { - borderColor: "secondary.main", - }, + "&:hover": disabled ? undefined : { borderColor: "secondary.main" }, }} > - {/* Dim only the card content while a download is required, so the - download control below keeps its normal color (CSS opacity on the - card would otherwise cap the button's opacity too). */} + {/* Dim only the card content while it is unusable, so the download + control below keeps its normal color (CSS opacity on the card would + otherwise cap the button's opacity too). */} {icon && ( @@ -190,6 +215,33 @@ function ComponentSelector({ /> )} + {(locked || optionalCredentials.length > 0) && ( + + {locked && ( + + + + )} + {!locked && optionalCredentials.length > 0 && ( + + + + )} + + )} {requiresDownload && ( e.stopPropagation()}> } credentialStatuses - name -> authenticated. + * @returns {{available: boolean, missingRequired: string[], missingOptional: string[]}} + */ +export function getComponentAvailability(component, credentialStatuses = {}) { + const required = component?.required_credentials ?? []; + const optional = component?.optional_credentials ?? []; + + const missingRequired = required.filter((name) => !credentialStatuses[name]); + const missingOptional = optional.filter((name) => !credentialStatuses[name]); + + const available = + typeof component?.credentials_satisfied === "boolean" + ? component.credentials_satisfied + : missingRequired.length === 0; + + return { available, missingRequired, missingOptional }; +} diff --git a/DashAI/front/src/types/component.ts b/DashAI/front/src/types/component.ts index f7fad0a32..26c69f12b 100644 --- a/DashAI/front/src/types/component.ts +++ b/DashAI/front/src/types/component.ts @@ -10,5 +10,8 @@ export interface IComponent { description: string; display_name?: string; color?: string; + required_credentials?: string[]; + optional_credentials?: string[]; + credentials_satisfied?: boolean; downloaded?: boolean; } diff --git a/DashAI/front/src/types/credential.ts b/DashAI/front/src/types/credential.ts new file mode 100644 index 000000000..9426156f6 --- /dev/null +++ b/DashAI/front/src/types/credential.ts @@ -0,0 +1,7 @@ +export interface ICredential { + name: string; + display_name: string; + description: string; + is_authenticated: boolean; + key: string | null; +} diff --git a/DashAI/front/src/utils/i18n/index.js b/DashAI/front/src/utils/i18n/index.js index 2e89068eb..493f4fdbe 100644 --- a/DashAI/front/src/utils/i18n/index.js +++ b/DashAI/front/src/utils/i18n/index.js @@ -37,6 +37,11 @@ import generativeTourEN from "./locales/en/generativeTour.json"; import generativeTourES from "./locales/es/generativeTour.json"; import hubEN from "./locales/en/hub.json"; import hubES from "./locales/es/hub.json"; +import credentialsEN from "./locales/en/credentials.json"; +import credentialsES from "./locales/es/credentials.json"; +import credentialsPT from "./locales/pt/credentials.json"; +import credentialsDE from "./locales/de/credentials.json"; +import credentialsZH from "./locales/zh/credentials.json"; import configurableObjectPT from "./locales/pt/configurableObject.json"; import commonPT from "./locales/pt/common.json"; import customPT from "./locales/pt/custom.json"; @@ -113,6 +118,7 @@ const resources = { modelsSessionTour: modelsSessionTourEN, generativeTour: generativeTourEN, hub: hubEN, + credentials: credentialsEN, }, es: { configurableObject: configurableObjectES, @@ -133,6 +139,7 @@ const resources = { modelsSessionTour: modelsSessionTourES, generativeTour: generativeTourES, hub: hubES, + credentials: credentialsES, }, pt: { configurableObject: configurableObjectPT, @@ -152,6 +159,7 @@ const resources = { modelsTour: modelsTourPT, modelsSessionTour: modelsSessionTourPT, generativeTour: generativeTourPT, + credentials: credentialsPT, }, de: { configurableObject: configurableObjectDE, @@ -171,6 +179,7 @@ const resources = { modelsTour: modelsTourDE, modelsSessionTour: modelsSessionTourDE, generativeTour: generativeTourDE, + credentials: credentialsDE, }, zh: { configurableObject: configurableObjectZH, @@ -191,6 +200,7 @@ const resources = { modelsSessionTour: modelsSessionTourZH, generativeTour: generativeTourZH, hub: hubZH, + credentials: credentialsZH, }, }; @@ -222,6 +232,7 @@ i18n "plugins", "generativeTour", "hub", + "credentials", ], defaultNS: "common", diff --git a/DashAI/front/src/utils/i18n/locales/de/credentials.json b/DashAI/front/src/utils/i18n/locales/de/credentials.json new file mode 100644 index 000000000..f1c54f253 --- /dev/null +++ b/DashAI/front/src/utils/i18n/locales/de/credentials.json @@ -0,0 +1,13 @@ +{ + "title": "Anmeldedaten", + "subtitle": "Bei externen Plattformen authentifizieren", + "keyPlaceholder": "API-Schluessel eingeben", + "verify": "Pruefen", + "remove": "Entfernen", + "authenticated": "Authentifiziert", + "notAuthenticated": "Nicht authentifiziert", + "verifySuccess": "Anmeldedaten geprueft", + "verifyError": "Ungueltiger Schluessel", + "requiredTooltip": "Bei {{platform}} authentifizieren, um dies zu nutzen", + "optionalTooltip": "Bei {{platform}} authentifizieren fuer zusaetzlichen Zugriff" +} diff --git a/DashAI/front/src/utils/i18n/locales/en/credentials.json b/DashAI/front/src/utils/i18n/locales/en/credentials.json new file mode 100644 index 000000000..fb120e96e --- /dev/null +++ b/DashAI/front/src/utils/i18n/locales/en/credentials.json @@ -0,0 +1,13 @@ +{ + "title": "Credentials", + "subtitle": "Authenticate to external platforms", + "keyPlaceholder": "Enter your API key", + "verify": "Verify", + "remove": "Remove", + "authenticated": "Authenticated", + "notAuthenticated": "Not authenticated", + "verifySuccess": "Credential verified", + "verifyError": "Invalid key", + "requiredTooltip": "Authenticate {{platform}} to use this", + "optionalTooltip": "Authenticate {{platform}} for extra access" +} diff --git a/DashAI/front/src/utils/i18n/locales/es/credentials.json b/DashAI/front/src/utils/i18n/locales/es/credentials.json new file mode 100644 index 000000000..f7c5a8116 --- /dev/null +++ b/DashAI/front/src/utils/i18n/locales/es/credentials.json @@ -0,0 +1,13 @@ +{ + "title": "Credenciales", + "subtitle": "Autenticarse en plataformas externas", + "keyPlaceholder": "Ingrese su clave de API", + "verify": "Verificar", + "remove": "Eliminar", + "authenticated": "Autenticado", + "notAuthenticated": "No autenticado", + "verifySuccess": "Credencial verificada", + "verifyError": "Clave invalida", + "requiredTooltip": "Autentiquese en {{platform}} para usar esto", + "optionalTooltip": "Autentiquese en {{platform}} para acceso adicional" +} diff --git a/DashAI/front/src/utils/i18n/locales/pt/credentials.json b/DashAI/front/src/utils/i18n/locales/pt/credentials.json new file mode 100644 index 000000000..cbb0d442a --- /dev/null +++ b/DashAI/front/src/utils/i18n/locales/pt/credentials.json @@ -0,0 +1,13 @@ +{ + "title": "Credenciais", + "subtitle": "Autenticar em plataformas externas", + "keyPlaceholder": "Insira sua chave de API", + "verify": "Verificar", + "remove": "Remover", + "authenticated": "Autenticado", + "notAuthenticated": "Nao autenticado", + "verifySuccess": "Credencial verificada", + "verifyError": "Chave invalida", + "requiredTooltip": "Autentique-se no {{platform}} para usar isto", + "optionalTooltip": "Autentique-se no {{platform}} para acesso adicional" +} diff --git a/DashAI/front/src/utils/i18n/locales/zh/credentials.json b/DashAI/front/src/utils/i18n/locales/zh/credentials.json new file mode 100644 index 000000000..7a7f3d7f4 --- /dev/null +++ b/DashAI/front/src/utils/i18n/locales/zh/credentials.json @@ -0,0 +1,13 @@ +{ + "title": "凭证", + "subtitle": "对外部平台进行身份验证", + "keyPlaceholder": "输入您的 API 密钥", + "verify": "验证", + "remove": "移除", + "authenticated": "已验证", + "notAuthenticated": "未验证", + "verifySuccess": "凭证已验证", + "verifyError": "无效的密钥", + "requiredTooltip": "验证 {{platform}} 以使用此功能", + "optionalTooltip": "验证 {{platform}} 以获得额外访问权限" +} diff --git a/pyproject.toml b/pyproject.toml index b7b221db6..76159e910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,8 @@ dependencies = [ "pywebview", "openml", "oslo.concurrency", + "cryptography>=49.0.0", + "kaggle>=1.7.4.5", ] [project.optional-dependencies] diff --git a/tests/back/api/test_components_api.py b/tests/back/api/test_components_api.py index 6f598d304..4deabfc2c 100644 --- a/tests/back/api/test_components_api.py +++ b/tests/back/api/test_components_api.py @@ -164,6 +164,9 @@ def test_get_component_by_id(client: TestClient): "description": "Task 1.", "display_name": "Test Task 1", "color": "#795548", + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } @@ -183,6 +186,9 @@ def test_get_component_by_id(client: TestClient): "description": "Task 2.", "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } @@ -201,6 +207,9 @@ def test_get_component_by_id(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } @@ -295,6 +304,9 @@ def test_get_components_select_only_tasks(client: TestClient): "description": "Task 1.", "display_name": "Test Task 1", "color": "#795548", + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -311,6 +323,9 @@ def test_get_components_select_only_tasks(client: TestClient): "description": "Task 2.", "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] @@ -335,6 +350,9 @@ def test_get_components_select_only_dataloaders(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -350,6 +368,9 @@ def test_get_components_select_only_dataloaders(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -365,6 +386,9 @@ def test_get_components_select_only_dataloaders(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] @@ -446,6 +470,9 @@ def test_get_components_ignore_models(client: TestClient): "description": "Task 1.", "display_name": "Test Task 1", "color": "#795548", + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -462,6 +489,9 @@ def test_get_components_ignore_models(client: TestClient): "description": "Task 2.", "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -477,6 +507,9 @@ def test_get_components_ignore_models(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -492,6 +525,9 @@ def test_get_components_ignore_models(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -507,6 +543,9 @@ def test_get_components_ignore_models(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] @@ -530,6 +569,9 @@ def test_get_components_ignore_tasks_and_models(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -545,6 +587,9 @@ def test_get_components_ignore_tasks_and_models(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -560,6 +605,9 @@ def test_get_components_ignore_tasks_and_models(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] @@ -624,6 +672,9 @@ def test_get_components_related_inverse_relation(client: TestClient): "description": "Task 1.", "display_name": "Test Task 1", "color": "#795548", + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } ] @@ -670,6 +721,9 @@ def test_get_components_dataloader_component_parent(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -685,6 +739,9 @@ def test_get_components_dataloader_component_parent(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] @@ -725,6 +782,9 @@ def test_get_components_by_type_and_task(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -740,6 +800,9 @@ def test_get_components_by_type_and_task(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] @@ -779,6 +842,9 @@ def test_get_components_select_and_ignore_by_type(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -794,6 +860,9 @@ def test_get_components_select_and_ignore_by_type(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -809,6 +878,9 @@ def test_get_components_select_and_ignore_by_type(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] @@ -835,6 +907,9 @@ def test_get_components_select_type_and_parent(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, { @@ -850,6 +925,9 @@ def test_get_components_select_type_and_parent(client: TestClient): "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, }, ] diff --git a/tests/back/api/test_credentials_api.py b/tests/back/api/test_credentials_api.py new file mode 100644 index 000000000..bc318eb5f --- /dev/null +++ b/tests/back/api/test_credentials_api.py @@ -0,0 +1,74 @@ +from unittest.mock import patch + + +def test_list_credentials(client): + response = client.get("/api/v1/credential/") + assert response.status_code == 200 + names = {c["name"] for c in response.json()} + assert "HuggingFaceCredential" in names + for cred in response.json(): + assert "is_authenticated" in cred + # catalog + status returned together in one request + assert "display_name" in cred + assert "description" in cred + assert "key" in cred + + +def test_auth_success_marks_authenticated(client): + with patch( + "DashAI.back.credentials.huggingface_credential.HuggingFaceCredential.verify", + return_value=True, + ): + response = client.post( + "/api/v1/credential/HuggingFaceCredential/auth", + json={"key": "hf_token"}, + ) + assert response.status_code == 200 + assert response.json()["is_authenticated"] is True + + status = client.get("/api/v1/credential/HuggingFaceCredential") + assert status.json()["is_authenticated"] is True + + +def test_auth_invalid_key_returns_400(client): + with patch( + "DashAI.back.credentials.huggingface_credential.HuggingFaceCredential.verify", + return_value=False, + ): + response = client.post( + "/api/v1/credential/HuggingFaceCredential/auth", + json={"key": "bad-secret-key"}, + ) + assert response.status_code == 400 + # the error must never echo the submitted key + assert "bad-secret-key" not in response.text + + +def test_auth_unknown_credential_returns_404(client): + response = client.post("/api/v1/credential/NotACredential/auth", json={"key": "x"}) + assert response.status_code == 404 + + +def test_get_unknown_credential_returns_404(client): + response = client.get("/api/v1/credential/NotACredential") + assert response.status_code == 404 + + +def test_delete_unknown_credential_returns_404(client): + response = client.delete("/api/v1/credential/NotACredential") + assert response.status_code == 404 + + +def test_delete_credential(client): + with patch( + "DashAI.back.credentials.huggingface_credential.HuggingFaceCredential.verify", + return_value=True, + ): + client.post( + "/api/v1/credential/HuggingFaceCredential/auth", + json={"key": "hf_token"}, + ) + response = client.delete("/api/v1/credential/HuggingFaceCredential") + assert response.status_code == 200 + status = client.get("/api/v1/credential/HuggingFaceCredential") + assert status.json()["is_authenticated"] is False diff --git a/tests/back/credentials/__init__.py b/tests/back/credentials/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/back/credentials/test_base_credential.py b/tests/back/credentials/test_base_credential.py new file mode 100644 index 000000000..6220a3a0e --- /dev/null +++ b/tests/back/credentials/test_base_credential.py @@ -0,0 +1,55 @@ +import pytest +from cryptography.fernet import Fernet +from kink import di +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from DashAI.back.credentials.base_credential import BaseCredential +from DashAI.back.credentials.encryptor import CredentialEncryptor +from DashAI.back.credentials.store import CredentialStore +from DashAI.back.dependencies.database.models import Base + + +class FakeCredential(BaseCredential): + DISPLAY_NAME = "Fake" + DESCRIPTION = "Fake credential for tests" + last_seen_key = None + + def verify(self, key: str) -> bool: + return key == "good" + + +@pytest.fixture(autouse=True) +def credential_store_in_di(): + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + session_factory = sessionmaker(bind=engine) + encryptor = CredentialEncryptor(Fernet.generate_key()) + di["credential_store"] = CredentialStore(session_factory, encryptor) + yield + del di["credential_store"] + + +def test_auth_stores_valid_key(): + cred = FakeCredential() + assert cred.auth("good") is True + assert cred.get_key() == "good" + assert cred.is_authenticated() is True + + +def test_auth_rejects_invalid_key(): + cred = FakeCredential() + with pytest.raises(ValueError, match="Invalid credential"): + cred.auth("bad") + assert cred.get_key() is None + assert cred.is_authenticated() is False + + +def test_apply_is_noop_without_key(): + cred = FakeCredential() + # should not raise even though nothing is stored + cred.apply() + + +def test_type_is_credential(): + assert FakeCredential.TYPE == "Credential" diff --git a/tests/back/credentials/test_concrete_credentials.py b/tests/back/credentials/test_concrete_credentials.py new file mode 100644 index 000000000..fd193fc7d --- /dev/null +++ b/tests/back/credentials/test_concrete_credentials.py @@ -0,0 +1,83 @@ +import sys +import types +from contextlib import contextmanager +from unittest.mock import MagicMock, patch + +from DashAI.back.credentials.github_credential import GithubCredential +from DashAI.back.credentials.huggingface_credential import HuggingFaceCredential +from DashAI.back.credentials.kaggle_credential import KaggleCredential + + +@contextmanager +def fake_kaggle(api_instance): + """Install a stub ``kaggle`` package so the real one is never imported. + + The official ``kaggle`` package authenticates at import time and exits the + process without credentials, so tests inject a fake module tree exposing a + ``KaggleApi`` that returns ``api_instance``. + """ + module_names = ("kaggle", "kaggle.api", "kaggle.api.kaggle_api_extended") + saved = {name: sys.modules.get(name) for name in module_names} + sys.modules["kaggle"] = types.ModuleType("kaggle") + sys.modules["kaggle.api"] = types.ModuleType("kaggle.api") + extended = types.ModuleType("kaggle.api.kaggle_api_extended") + extended.KaggleApi = MagicMock(return_value=api_instance) + sys.modules["kaggle.api.kaggle_api_extended"] = extended + try: + yield + finally: + for name, module in saved.items(): + if module is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = module + + +def test_huggingface_verify_success(): + cred = HuggingFaceCredential() + with patch("huggingface_hub.HfApi") as hf_api: + hf_api.return_value.whoami.return_value = {"name": "user"} + assert cred.verify("hf_good") is True + + +def test_huggingface_verify_failure(): + cred = HuggingFaceCredential() + with patch("huggingface_hub.HfApi") as hf_api: + hf_api.return_value.whoami.side_effect = Exception("401") + assert cred.verify("hf_bad") is False + + +def test_github_verify_success(): + cred = GithubCredential() + with patch("requests.get") as get: + get.return_value = MagicMock(status_code=200) + assert cred.verify("ghp_good") is True + + +def test_github_verify_failure(): + cred = GithubCredential() + with patch("requests.get") as get: + get.return_value = MagicMock(status_code=401) + assert cred.verify("ghp_bad") is False + + +def test_kaggle_verify_success(): + cred = KaggleCredential() + api = MagicMock() + api.authenticate.return_value = None + api.competitions_list.return_value = [] + with fake_kaggle(api): + assert cred.verify("user:key") is True + + +def test_kaggle_verify_failure(): + cred = KaggleCredential() + api = MagicMock() + api.competitions_list.side_effect = Exception("401") + with fake_kaggle(api): + assert cred.verify("user:badkey") is False + + +def test_kaggle_verify_malformed_key(): + cred = KaggleCredential() + assert cred.verify("no-separator") is False diff --git a/tests/back/credentials/test_encryptor.py b/tests/back/credentials/test_encryptor.py new file mode 100644 index 000000000..686762465 --- /dev/null +++ b/tests/back/credentials/test_encryptor.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from DashAI.back.credentials.encryptor import ( + CredentialEncryptor, + load_or_create_key, +) + + +def test_encrypt_decrypt_roundtrip(): + key = load_or_create_key(Path("/nonexistent/path"), env_value=None, persist=False) + enc = CredentialEncryptor(key) + token = enc.encrypt("hf_secret_token") + assert token != "hf_secret_token" + assert enc.decrypt(token) == "hf_secret_token" + + +def test_load_or_create_key_uses_env_value(tmp_path): + from cryptography.fernet import Fernet + + env_key = Fernet.generate_key().decode() + key = load_or_create_key(tmp_path / "key", env_value=env_key) + assert key == env_key.encode() + + +def test_load_or_create_key_persists_and_reuses(tmp_path): + key_path = tmp_path / ".credentials_key" + first = load_or_create_key(key_path, env_value=None) + assert key_path.exists() + second = load_or_create_key(key_path, env_value=None) + assert first == second diff --git a/tests/back/credentials/test_get_credential.py b/tests/back/credentials/test_get_credential.py new file mode 100644 index 000000000..f08b7105d --- /dev/null +++ b/tests/back/credentials/test_get_credential.py @@ -0,0 +1,32 @@ +from kink import di + +from DashAI.back.config_object import ConfigObject +from DashAI.back.credentials.base_credential import BaseCredential +from DashAI.back.dependencies.registry import ComponentRegistry + + +class DummyCredential(BaseCredential): + DISPLAY_NAME = "Dummy" + DESCRIPTION = "dummy" + + def verify(self, key: str) -> bool: + return True + + +class DummyComponentBase: + TYPE = "DummyType" + + +class DummyComponent(ConfigObject, DummyComponentBase): + REQUIRED_CREDENTIALS = ["DummyCredential"] + + +def test_get_credential_returns_instance(): + registry = ComponentRegistry(initial_components=[DummyCredential]) + di["component_registry"] = registry + try: + component = DummyComponent() + cred = component.get_credential("DummyCredential") + assert isinstance(cred, DummyCredential) + finally: + del di["component_registry"] diff --git a/tests/back/credentials/test_store.py b/tests/back/credentials/test_store.py new file mode 100644 index 000000000..837ef4b56 --- /dev/null +++ b/tests/back/credentials/test_store.py @@ -0,0 +1,58 @@ +import pytest +from cryptography.fernet import Fernet +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from DashAI.back.credentials.encryptor import CredentialEncryptor +from DashAI.back.credentials.store import CredentialStore +from DashAI.back.dependencies.database.models import Base + + +@pytest.fixture +def session_factory(): + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + return sessionmaker(bind=engine) + + +@pytest.fixture +def store(session_factory): + encryptor = CredentialEncryptor(Fernet.generate_key()) + return CredentialStore(session_factory, encryptor) + + +def test_save_and_load_roundtrip(store): + store.save("HuggingFaceCredential", "hf_token_123") + assert store.load("HuggingFaceCredential") == "hf_token_123" + + +def test_save_marks_verified(store): + store.save("HuggingFaceCredential", "hf_token_123") + assert store.is_verified("HuggingFaceCredential") is True + + +def test_load_missing_returns_none(store): + assert store.load("Missing") is None + assert store.is_verified("Missing") is False + + +def test_save_is_upsert(store): + store.save("HuggingFaceCredential", "old") + store.save("HuggingFaceCredential", "new") + assert store.load("HuggingFaceCredential") == "new" + + +def test_delete_removes_key(store): + store.save("HuggingFaceCredential", "tok") + store.delete("HuggingFaceCredential") + assert store.load("HuggingFaceCredential") is None + assert store.is_verified("HuggingFaceCredential") is False + + +def test_all_statuses(store): + store.save("HuggingFaceCredential", "tok") + store.save("GithubCredential", "ghp") + assert store.all_statuses() == { + "HuggingFaceCredential": True, + "GithubCredential": True, + } diff --git a/tests/back/registries/test_registry.py b/tests/back/registries/test_registry.py index eabd08f44..d7ee1c126 100644 --- a/tests/back/registries/test_registry.py +++ b/tests/back/registries/test_registry.py @@ -80,6 +80,9 @@ class NoComponent: ... "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } COMPONENT2_DICT = { @@ -92,6 +95,9 @@ class NoComponent: ... "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } SUBCOMPONENT1_DICT = { @@ -104,6 +110,9 @@ class NoComponent: ... "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } COMPONENT3_DICT = { @@ -116,6 +125,9 @@ class NoComponent: ... "description": "Some static component", "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } COMPONENT3_DICT_MS = COMPONENT3_DICT.copy() @@ -131,6 +143,9 @@ class NoComponent: ... "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } RELATED_COMPONENT2_DICT = { @@ -143,6 +158,9 @@ class NoComponent: ... "description": None, "display_name": None, "color": None, + "required_credentials": [], + "optional_credentials": [], + "credentials_satisfied": True, "downloaded": True, } @@ -463,10 +481,12 @@ def test_relationships_module(): test_registry.register_component(RelatedComponent2) assert test_registry._relationship_manager.relations == { - "RelatedComponent1": ["Component1"], - "Component1": ["RelatedComponent1", "RelatedComponent2"], - "RelatedComponent2": ["Component1", "Component2"], - "Component2": ["RelatedComponent2"], + "RelatedComponent1": {"compatible_components": ["Component1"]}, + "Component1": { + "compatible_components": ["RelatedComponent1", "RelatedComponent2"] + }, + "RelatedComponent2": {"compatible_components": ["Component1", "Component2"]}, + "Component2": {"compatible_components": ["RelatedComponent2"]}, } assert test_registry.get_related_components("Component1") == [ @@ -488,3 +508,59 @@ def test_relationships_module(): COMPONENT1_DICT, COMPONENT2_DICT, ] + + +class CredentialComponentA: + TYPE = "Credential" + + +class ComponentNeedsCred(BaseStaticComponent): + REQUIRED_CREDENTIALS = ["CredentialComponentA"] + + +class ComponentOptionalCred(BaseStaticComponent): + OPTIONAL_CREDENTIALS = ["CredentialComponentA"] + + +def test_register_records_credential_relations_and_flag(): + reg = ComponentRegistry( + initial_components=[ComponentNeedsCred, ComponentOptionalCred] + ) + + # required credential present -> not satisfied until verified + assert reg["ComponentNeedsCred"]["required_credentials"] == ["CredentialComponentA"] + assert reg["ComponentNeedsCred"]["optional_credentials"] == [] + assert reg["ComponentNeedsCred"]["credentials_satisfied"] is False + + # only optional credential -> always satisfied + assert reg["ComponentOptionalCred"]["optional_credentials"] == [ + "CredentialComponentA" + ] + assert reg["ComponentOptionalCred"]["credentials_satisfied"] is True + + assert reg.get_required_credentials("ComponentNeedsCred") == [ + "CredentialComponentA" + ] + assert reg.get_optional_credentials("ComponentOptionalCred") == [ + "CredentialComponentA" + ] + + +def test_refresh_credentials_status_updates_flag(): + reg = ComponentRegistry(initial_components=[ComponentNeedsCred]) + assert reg["ComponentNeedsCred"]["credentials_satisfied"] is False + + reg.refresh_credentials_status({"CredentialComponentA": True}) + assert reg["ComponentNeedsCred"]["credentials_satisfied"] is True + + reg.refresh_credentials_status({"CredentialComponentA": False}) + assert reg["ComponentNeedsCred"]["credentials_satisfied"] is False + + +def test_refresh_credentials_status_only_targets_subset(): + reg = ComponentRegistry(initial_components=[ComponentNeedsCred]) + reg.refresh_credentials_status( + {"CredentialComponentA": True}, only=["SomeOtherComponent"] + ) + # not in `only` -> unchanged + assert reg["ComponentNeedsCred"]["credentials_satisfied"] is False diff --git a/tests/back/registries/test_relationship_manager.py b/tests/back/registries/test_relationship_manager.py index 7109f2ad9..f88e5e2f3 100644 --- a/tests/back/registries/test_relationship_manager.py +++ b/tests/back/registries/test_relationship_manager.py @@ -1,43 +1,40 @@ -from collections import defaultdict - from DashAI.back.dependencies.registry.relationship_manager import RelationshipManager -def test_relationship_manager_add_relations(): - test_relationship_manager = RelationshipManager() - - assert isinstance(test_relationship_manager.relations, dict) - assert isinstance(test_relationship_manager._relations, defaultdict) - assert test_relationship_manager.relations == {} - assert test_relationship_manager._relations == defaultdict(list) +def test_add_default_relation_type_is_bidirectional(): + rm = RelationshipManager() + rm.add_relationship("A", "B") + assert rm.get("A", "compatible_components") == ["B"] + assert rm.get("B", "compatible_components") == ["A"] - test_relationship_manager.add_relationship("Component1", "Task1") - test_relationship_manager.add_relationship("Component2", "Task1") - test_relationship_manager.add_relationship("Component3", "Task2") - assert test_relationship_manager.relations == { - "Component1": ["Task1"], - "Task1": ["Component1", "Component2"], - "Component2": ["Task1"], - "Component3": ["Task2"], - "Task2": ["Component3"], - } +def test_relation_types_are_isolated(): + rm = RelationshipManager() + rm.add_relationship("Model", "Task", "compatible_components") + rm.add_relationship("Model", "HFCred", "required_credentials") + assert rm.get("Model", "compatible_components") == ["Task"] + assert rm.get("Model", "required_credentials") == ["HFCred"] + # reverse lookup of who requires the credential + assert rm.get("HFCred", "required_credentials") == ["Model"] -def test_relationship_manager__getitem__(): - test_relationship_manager = RelationshipManager() +def test_get_missing_returns_empty_list(): + rm = RelationshipManager() + assert rm.get("X", "compatible_components") == [] - test_relationship_manager.add_relationship("Component1", "Task1") - test_relationship_manager.add_relationship("Component2", "Task1") - test_relationship_manager.add_relationship("Component3", "Task2") - assert test_relationship_manager["Component1"] == ["Task1"] - assert test_relationship_manager["Component2"] == ["Task1"] - assert test_relationship_manager["Component3"] == ["Task2"] - assert test_relationship_manager["Task1"] == ["Component1", "Component2"] - assert test_relationship_manager["Task2"] == ["Component3"] +def test_relations_property_is_nested(): + rm = RelationshipManager() + rm.add_relationship("A", "B") + assert rm.relations == { + "A": {"compatible_components": ["B"]}, + "B": {"compatible_components": ["A"]}, + } -def test_relationship_manager__getitem__unexistant_component(): - test_relationship_manager = RelationshipManager() - assert test_relationship_manager["UnexistantComponent"] == [] +def test_remove_relationship_with_type(): + rm = RelationshipManager() + rm.add_relationship("A", "B", "required_credentials") + rm.remove_relationship("A", "B", "required_credentials") + assert rm.get("A", "required_credentials") == [] + assert rm.get("B", "required_credentials") == [] diff --git a/uv.lock b/uv.lock index 75de2dd8d..b714218b3 100644 --- a/uv.lock +++ b/uv.lock @@ -599,6 +599,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] +[[package]] +name = "bleach" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, +] + [[package]] name = "bottle" version = "0.13.4" @@ -1229,6 +1241,63 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, ] +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + [[package]] name = "cuda-bindings" version = "12.9.7" @@ -1431,6 +1500,7 @@ dependencies = [ { name = "beartype" }, { name = "cmaes" }, { name = "controlnet-aux" }, + { name = "cryptography" }, { name = "datasets" }, { name = "diffusers" }, { name = "evaluate" }, @@ -1443,6 +1513,8 @@ dependencies = [ { name = "ijson" }, { name = "imblearn" }, { name = "joblib" }, + { name = "kaggle", version = "1.7.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "kaggle", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, { name = "kink" }, { name = "llvmlite" }, { name = "numba", version = "0.47.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.12' and extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda') or (platform_machine != 'x86_64' and extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda') or (sys_platform != 'darwin' and extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, @@ -1526,6 +1598,7 @@ requires-dist = [ { name = "beartype" }, { name = "cmaes" }, { name = "controlnet-aux" }, + { name = "cryptography", specifier = ">=49.0.0" }, { name = "datasets" }, { name = "diffusers" }, { name = "evaluate" }, @@ -1538,6 +1611,7 @@ requires-dist = [ { name = "ijson" }, { name = "imblearn" }, { name = "joblib" }, + { name = "kaggle", specifier = ">=1.7.4.5" }, { name = "kink" }, { name = "llama-cpp-python", marker = "extra == 'cpu'", index = "https://abetlen.github.io/llama-cpp-python/whl/cpu", conflict = { package = "dashai", extra = "cpu" } }, { name = "llama-cpp-python", marker = "extra == 'cuda'" }, @@ -2033,6 +2107,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl", hash = "sha256:9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7", size = 18702, upload-time = "2025-08-11T10:19:35.716Z" }, ] +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + [[package]] name = "filelock" version = "3.29.5" @@ -2820,6 +2903,162 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "referencing", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "rpds-py", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "traitlets", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupytext" +version = "1.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "mdit-py-plugins", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "nbformat", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "pyyaml", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/473f8ebb101553fb2ea6ab1d34324d6677844c968947ac050c759d539f2c/jupytext-1.19.5.tar.gz", hash = "sha256:605026446d605aa54fd7f7fc69df6ae51c7a46053d4cebf05afdc64d66de3df0", size = 4600916, upload-time = "2026-07-21T22:00:29.198Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/c6/76ee9dacedcd8c67d8fa53dd975613733bdd28242a4c41518ff1c8aeaa64/jupytext-1.19.5-py3-none-any.whl", hash = "sha256:af22351202171116b986fe1f4f9233a13ca4a85d5eec57a87900ed48521dbae4", size = 229246, upload-time = "2026-07-21T22:00:27.218Z" }, +] + +[[package]] +name = "kaggle" +version = "1.7.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda'", + "python_full_version < '3.11' and sys_platform != 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version < '3.11' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", +] +dependencies = [ + { name = "bleach", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "certifi", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "charset-normalizer", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "idna", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "protobuf", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "python-dateutil", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "python-slugify", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "requests", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "setuptools", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "six", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "text-unidecode", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "tqdm", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "urllib3", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "webencodings", marker = "python_full_version < '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/02/b0c189a46531ea2b2691ae277508d2c80e5fd3d757083283c5cc27800ca8/kaggle-1.7.4.5.tar.gz", hash = "sha256:1d9821bd6a6a1470741c76d26495a18475b5a7bfe0c80b19191254b2735d41dd", size = 336100, upload-time = "2025-05-08T21:17:20.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/83/7f29c7abe0d5dc769dad7da993382c3e4239ad63e1dd58414d129e0a4da2/kaggle-1.7.4.5-py3-none-any.whl", hash = "sha256:9732afb1c073f14acc7e49dfab98456f887d10b735bcd6348bc1340e92393882", size = 181238, upload-time = "2025-05-08T21:17:18.245Z" }, +] + +[[package]] +name = "kaggle" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda'", + "python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda'", + "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda'", + "(python_full_version >= '3.15' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda') or (python_full_version >= '3.15' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')", + "(python_full_version == '3.14.*' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda') or (python_full_version == '3.14.*' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')", + "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')", + "python_full_version == '3.11.*' and extra != 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda'", + "python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version >= '3.15' and sys_platform != 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.14.*' and sys_platform != 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version >= '3.15' and platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.14.*' and platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.12.*' and platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra == 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", + "(python_full_version >= '3.15' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda') or (python_full_version >= '3.15' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda')", + "(python_full_version == '3.14.*' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda') or (python_full_version == '3.14.*' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda')", + "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda')", + "python_full_version == '3.11.*' and extra != 'extra-6-dashai-cpu' and extra != 'extra-6-dashai-cuda'", +] +dependencies = [ + { name = "bleach", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "jupytext", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "kagglesdk", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "protobuf", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "python-dotenv", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "python-slugify", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "requests", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "tqdm", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "urllib3", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/cf/2e5b5e0710844ce78fbdae4630527f8e2c23c0851c0a215404b8a6c981b0/kaggle-2.2.3.tar.gz", hash = "sha256:ce292c459a13950553d894ee7368d6454be53291bd75c2d27ad2bed149cdd6c4", size = 245527, upload-time = "2026-06-25T19:47:35.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/46/24e3db0c32cd3e089816bd605ab39fae72a0ae688c677cc9b056789c5f3c/kaggle-2.2.3-py3-none-any.whl", hash = "sha256:535eed6612910979ff3025f9c213207718305d47ab5f263fe74acd735c7e902e", size = 111507, upload-time = "2026-06-25T19:47:34.724Z" }, +] + +[[package]] +name = "kagglesdk" +version = "0.1.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "requests", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/3f/2b7b46e8c17046261b138f22c5b7421bbbb85cf6a8f69f4aa43fe983eb1a/kagglesdk-0.1.35.tar.gz", hash = "sha256:7373c46e7546dc983e1cd87fb7555906f799869b6f02a747a3121c89edc420b8", size = 198490, upload-time = "2026-07-22T21:54:32.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/96/37de4e0442378ac96e324123418b81cf5be0e9c7f7236fddae6641b20db2/kagglesdk-0.1.35-py3-none-any.whl", hash = "sha256:af662aedfad00aa5d5e3dd946c1a82b5117cf4964f6f9e96cea6e83b77419e17", size = 248627, upload-time = "2026-07-22T21:54:30.737Z" }, +] + [[package]] name = "kink" version = "0.9.0" @@ -3591,6 +3830,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/d8/c4ecab06b7ea36a570c4f3bd2d48d1799fd5d9174470e45c2194199431e7/matplotlib-3.11.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf662e5ac5707658cb931e19972c4bd99f7b4f8b7bf79d3c821d239fa6b71e64", size = 10015653, upload-time = "2026-06-12T02:29:13.251Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -3889,6 +4140,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/4e/afc8c31605cb8be1d3bb4438c4d979daa104dab6306cd2b87abe9c3a7299/narwhals-2.23.0-py3-none-any.whl", hash = "sha256:769e7b9ab102c93d8fa019f6b4cd1a657909b04a20bf6210e5a35aae06814ae9", size = 458938, upload-time = "2026-07-01T11:21:51.677Z" }, ] +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "jsonschema", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "jupyter-core", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "traitlets", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + [[package]] name = "netaddr" version = "1.3.0" @@ -6016,6 +6282,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + [[package]] name = "pythonnet" version = "3.1.0" @@ -6161,6 +6439,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "rpds-py", marker = "python_full_version >= '3.11' or (extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version < '3.11' and extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda') or (python_full_version >= '3.13' and extra == 'extra-6-dashai-cpu' and extra == 'extra-6-dashai-cuda')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "regex" version = "2026.6.28" @@ -6463,6 +6755,129 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + [[package]] name = "ruff" version = "0.15.20" @@ -7610,6 +8025,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + [[package]] name = "threadpoolctl" version = "3.6.0" @@ -8216,6 +8640,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] +[[package]] +name = "traitlets" +version = "5.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, +] + [[package]] name = "transformers" version = "5.12.1" @@ -8550,6 +8983,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, ] +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + [[package]] name = "websockets" version = "16.0"