From 98b85a8d06e181919d724716b616bebdd63ecd24 Mon Sep 17 00:00:00 2001 From: Talha Date: Fri, 31 Jul 2026 00:18:34 +0500 Subject: [PATCH 1/8] feat(platform): add durable media and search services Build persistent media, job, snapshot, artifact, and repository services around capability-owned indexing and retrieval providers. --- src/vidxp/app_paths.py | 43 + src/vidxp/application.py | 1049 +++++++++---- src/vidxp/application_boundary.py | 190 +++ src/vidxp/application_models.py | 1081 ++++++++++++++ src/vidxp/artifact_service.py | 378 +++++ src/vidxp/authentication.py | 262 ++++ src/vidxp/authorization.py | 38 + src/vidxp/benchmarks/cli.py | 398 ++++- src/vidxp/benchmarks/common.py | 38 +- src/vidxp/benchmarks/didemo.py | 114 +- src/vidxp/benchmarks/hirest.py | 99 +- src/vidxp/benchmarks/prepare.py | 879 +++++++++++ src/vidxp/branding.py | 37 + src/vidxp/capabilities/__init__.py | 14 +- src/vidxp/capabilities/actor/config.py | 4 +- src/vidxp/capabilities/actor/definition.py | 94 +- src/vidxp/capabilities/actor/indexing.py | 156 +- src/vidxp/capabilities/actor/models.py | 70 + src/vidxp/capabilities/actor/operations.py | 48 +- src/vidxp/capabilities/actor/requirements.txt | 6 +- src/vidxp/capabilities/actor/results.py | 339 ++++- src/vidxp/capabilities/actor/schemas.py | 57 +- src/vidxp/capabilities/actor/specs.py | 51 + src/vidxp/capabilities/contracts.py | 195 ++- src/vidxp/capabilities/dialogue/config.py | 6 - src/vidxp/capabilities/dialogue/definition.py | 102 +- src/vidxp/capabilities/dialogue/indexing.py | 128 +- src/vidxp/capabilities/dialogue/models.py | 110 +- src/vidxp/capabilities/dialogue/operations.py | 27 +- .../capabilities/dialogue/requirements.txt | 6 +- src/vidxp/capabilities/dialogue/specs.py | 34 + src/vidxp/capabilities/registry.py | 682 +++++++-- src/vidxp/capabilities/scene/config.py | 2 +- src/vidxp/capabilities/scene/definition.py | 61 +- src/vidxp/capabilities/scene/indexing.py | 62 +- src/vidxp/capabilities/scene/models.py | 62 +- src/vidxp/capabilities/scene/operations.py | 36 +- src/vidxp/capabilities/scene/requirements.txt | 13 +- src/vidxp/capabilities/scene/specs.py | 16 + src/vidxp/capabilities/schemas.py | 46 +- src/vidxp/capabilities/search.py | 48 +- src/vidxp/capabilities/visual.py | 93 +- src/vidxp/capability_service.py | 48 + src/vidxp/composition.py | 498 +++++++ src/vidxp/control_plane.py | 196 +++ src/vidxp/core/artifacts.py | 93 ++ src/vidxp/core/contracts.py | 75 +- src/vidxp/core/cursors.py | 92 ++ src/vidxp/core/generations.py | 115 ++ src/vidxp/core/identifiers.py | 80 + src/vidxp/core/manifest.py | 157 +- src/vidxp/core/media.py | 190 +++ src/vidxp/core/runner.py | 274 ++-- src/vidxp/core/snapshots.py | 85 ++ src/vidxp/core/storage.py | 309 +++- src/vidxp/core/storage_keys.py | 13 + src/vidxp/core/uploads.py | 79 + src/vidxp/core/video.py | 132 +- src/vidxp/execution.py | 42 + src/vidxp/idempotency.py | 63 + src/vidxp/index_state.py | 128 +- src/vidxp/index_worker.py | 82 - src/vidxp/infrastructure/__init__.py | 1 + src/vidxp/infrastructure/dbos_jobs.py | 455 ++++++ src/vidxp/infrastructure/dbos_workflows.py | 359 +++++ src/vidxp/infrastructure/local_artifacts.py | 225 +++ src/vidxp/infrastructure/local_catalog.py | 63 + src/vidxp/infrastructure/local_files.py | 95 ++ src/vidxp/infrastructure/local_index.py | 575 +++++++ src/vidxp/infrastructure/local_media.py | 315 ++++ src/vidxp/infrastructure/local_objects.py | 80 + src/vidxp/infrastructure/local_snapshots.py | 482 ++++++ src/vidxp/infrastructure/local_worker.py | 331 ++++ src/vidxp/infrastructure/ollama_query.py | 106 ++ src/vidxp/infrastructure/sql_catalog.py | 698 +++++++++ src/vidxp/infrastructure/sql_snapshots.py | 371 +++++ src/vidxp/infrastructure/sql_tables.py | 144 ++ src/vidxp/infrastructure/tusd_contracts.py | 97 ++ src/vidxp/job_service.py | 340 +++++ src/vidxp/media_runtime.py | 317 ++++ src/vidxp/media_service.py | 313 ++++ src/vidxp/migrations/__init__.py | 1 + src/vidxp/migrations/env.py | 39 + .../versions/20260729_01_catalog_uploads.py | 173 +++ src/vidxp/migrations/versions/__init__.py | 1 + src/vidxp/model_contracts.py | 154 ++ src/vidxp/ports.py | 413 +++++ src/vidxp/query_service.py | 291 ++++ src/vidxp/read_job_planner.py | 75 + src/vidxp/readiness_service.py | 88 ++ src/vidxp/repositories.py | 53 +- src/vidxp/repository_layout.py | 84 ++ src/vidxp/requirements/frontend.txt | 2 +- src/vidxp/requirements/mcp.txt | 1 + src/vidxp/requirements/server-storage.txt | 1 + src/vidxp/requirements/server.txt | 8 + src/vidxp/requirements/slm.txt | 1 + src/vidxp/requirements/storage.txt | 3 +- src/vidxp/requirements/test.txt | 2 + src/vidxp/runtime.py | 407 +++++ src/vidxp/search_fusion.py | 160 ++ src/vidxp/settings.py | 673 +++++++++ src/vidxp/tusd_hooks.py | 144 ++ src/vidxp/upload_service.py | 659 ++++++++ src/vidxp/workflow_contracts.py | 53 + src/vidxp/workflow_runtime.py | 130 ++ src/vidxp/workflow_worker.py | 256 ++++ tests/test_actor_results.py | 309 +++- tests/test_application.py | 1327 ++++++++++++++--- tests/test_application_boundary.py | 43 + tests/test_authentication.py | 339 +++++ tests/test_benchmark_cli.py | 227 ++- tests/test_benchmark_prepare.py | 385 +++++ tests/test_benchmarks.py | 84 +- tests/test_capabilities.py | 459 ++++-- tests/test_contracts.py | 30 +- tests/test_cursors.py | 56 + tests/test_dbos_jobs.py | 558 +++++++ tests/test_generation_manifest.py | 190 +++ tests/test_index_state.py | 95 -- tests/test_index_worker.py | 150 -- tests/test_indexing.py | 643 ++++---- tests/test_job_contracts.py | 308 ++++ tests/test_local_snapshots.py | 798 ++++++++++ tests/test_local_worker.py | 485 ++++++ tests/test_manifest.py | 141 +- tests/test_media_catalog.py | 451 ++++++ tests/test_media_runtime.py | 144 ++ tests/test_media_services.py | 827 ++++++++++ tests/test_models.py | 580 ++++++- tests/test_ollama_query.py | 136 ++ tests/test_packaging.py | 296 +++- tests/test_query_service.py | 377 +++++ tests/test_read_job_planner.py | 73 + tests/test_repositories.py | 53 +- tests/test_runner.py | 170 ++- tests/test_search.py | 45 +- tests/test_search_fusion.py | 114 ++ tests/test_settings.py | 121 ++ tests/test_snapshots.py | 168 +++ tests/test_storage.py | 210 ++- tests/test_storage_integration.py | 82 + tests/test_tusd_hooks.py | 152 ++ tests/test_upload_service.py | 424 ++++++ tests/test_video.py | 59 +- 145 files changed, 27802 insertions(+), 2471 deletions(-) create mode 100644 src/vidxp/app_paths.py create mode 100644 src/vidxp/application_boundary.py create mode 100644 src/vidxp/application_models.py create mode 100644 src/vidxp/artifact_service.py create mode 100644 src/vidxp/authentication.py create mode 100644 src/vidxp/authorization.py create mode 100644 src/vidxp/benchmarks/prepare.py create mode 100644 src/vidxp/branding.py create mode 100644 src/vidxp/capabilities/actor/models.py create mode 100644 src/vidxp/capabilities/actor/specs.py create mode 100644 src/vidxp/capabilities/dialogue/specs.py create mode 100644 src/vidxp/capabilities/scene/specs.py create mode 100644 src/vidxp/capability_service.py create mode 100644 src/vidxp/composition.py create mode 100644 src/vidxp/control_plane.py create mode 100644 src/vidxp/core/artifacts.py create mode 100644 src/vidxp/core/cursors.py create mode 100644 src/vidxp/core/generations.py create mode 100644 src/vidxp/core/identifiers.py create mode 100644 src/vidxp/core/media.py create mode 100644 src/vidxp/core/snapshots.py create mode 100644 src/vidxp/core/storage_keys.py create mode 100644 src/vidxp/core/uploads.py create mode 100644 src/vidxp/execution.py create mode 100644 src/vidxp/idempotency.py delete mode 100644 src/vidxp/index_worker.py create mode 100644 src/vidxp/infrastructure/__init__.py create mode 100644 src/vidxp/infrastructure/dbos_jobs.py create mode 100644 src/vidxp/infrastructure/dbos_workflows.py create mode 100644 src/vidxp/infrastructure/local_artifacts.py create mode 100644 src/vidxp/infrastructure/local_catalog.py create mode 100644 src/vidxp/infrastructure/local_files.py create mode 100644 src/vidxp/infrastructure/local_index.py create mode 100644 src/vidxp/infrastructure/local_media.py create mode 100644 src/vidxp/infrastructure/local_objects.py create mode 100644 src/vidxp/infrastructure/local_snapshots.py create mode 100644 src/vidxp/infrastructure/local_worker.py create mode 100644 src/vidxp/infrastructure/ollama_query.py create mode 100644 src/vidxp/infrastructure/sql_catalog.py create mode 100644 src/vidxp/infrastructure/sql_snapshots.py create mode 100644 src/vidxp/infrastructure/sql_tables.py create mode 100644 src/vidxp/infrastructure/tusd_contracts.py create mode 100644 src/vidxp/job_service.py create mode 100644 src/vidxp/media_runtime.py create mode 100644 src/vidxp/media_service.py create mode 100644 src/vidxp/migrations/__init__.py create mode 100644 src/vidxp/migrations/env.py create mode 100644 src/vidxp/migrations/versions/20260729_01_catalog_uploads.py create mode 100644 src/vidxp/migrations/versions/__init__.py create mode 100644 src/vidxp/model_contracts.py create mode 100644 src/vidxp/ports.py create mode 100644 src/vidxp/query_service.py create mode 100644 src/vidxp/read_job_planner.py create mode 100644 src/vidxp/readiness_service.py create mode 100644 src/vidxp/repository_layout.py create mode 100644 src/vidxp/requirements/mcp.txt create mode 100644 src/vidxp/requirements/server-storage.txt create mode 100644 src/vidxp/requirements/server.txt create mode 100644 src/vidxp/requirements/slm.txt create mode 100644 src/vidxp/requirements/test.txt create mode 100644 src/vidxp/runtime.py create mode 100644 src/vidxp/search_fusion.py create mode 100644 src/vidxp/settings.py create mode 100644 src/vidxp/tusd_hooks.py create mode 100644 src/vidxp/upload_service.py create mode 100644 src/vidxp/workflow_contracts.py create mode 100644 src/vidxp/workflow_runtime.py create mode 100644 src/vidxp/workflow_worker.py create mode 100644 tests/test_application_boundary.py create mode 100644 tests/test_authentication.py create mode 100644 tests/test_benchmark_prepare.py create mode 100644 tests/test_cursors.py create mode 100644 tests/test_dbos_jobs.py create mode 100644 tests/test_generation_manifest.py delete mode 100644 tests/test_index_state.py delete mode 100644 tests/test_index_worker.py create mode 100644 tests/test_job_contracts.py create mode 100644 tests/test_local_snapshots.py create mode 100644 tests/test_local_worker.py create mode 100644 tests/test_media_catalog.py create mode 100644 tests/test_media_runtime.py create mode 100644 tests/test_media_services.py create mode 100644 tests/test_ollama_query.py create mode 100644 tests/test_query_service.py create mode 100644 tests/test_read_job_planner.py create mode 100644 tests/test_search_fusion.py create mode 100644 tests/test_settings.py create mode 100644 tests/test_snapshots.py create mode 100644 tests/test_tusd_hooks.py create mode 100644 tests/test_upload_service.py diff --git a/src/vidxp/app_paths.py b/src/vidxp/app_paths.py new file mode 100644 index 0000000..ca12d63 --- /dev/null +++ b/src/vidxp/app_paths.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import os +import shutil +from pathlib import Path + +from platformdirs import user_config_path, user_data_path + + +APP_NAME = "VidXP" + + +def default_data_directory() -> Path: + configured = os.environ.get("VIDXP_DATA_DIR") + if configured: + return Path(configured).expanduser() + return user_data_path(APP_NAME, appauthor=False) + + +def default_repository_directory(data_directory: Path | None = None) -> Path: + root = data_directory or default_data_directory() + return root / "repositories" / "default" + + +def default_model_directory(data_directory: Path | None = None) -> Path: + root = data_directory or default_data_directory() + return root / "models" + + +def default_config_directory() -> Path: + return user_config_path(APP_NAME, appauthor=False, roaming=True) + + +def available_storage_bytes(directory: Path) -> int | None: + """Return free bytes on the volume that will contain ``directory``.""" + + candidate = directory.expanduser().resolve(strict=False) + while not candidate.exists() and candidate != candidate.parent: + candidate = candidate.parent + try: + return shutil.disk_usage(candidate).free + except OSError: + return None diff --git a/src/vidxp/application.py b/src/vidxp/application.py index 57223d0..fcbd1cf 100644 --- a/src/vidxp/application.py +++ b/src/vidxp/application.py @@ -1,358 +1,891 @@ from __future__ import annotations -from dataclasses import replace +from contextlib import contextmanager from pathlib import Path -from typing import Any, Iterable, Mapping, cast +from shutil import which +from time import perf_counter +from typing import Any, Callable, Iterator, Mapping, cast from pydantic import BaseModel -from vidxp.capabilities.contracts import ( - CapabilityContext, - PreparationContext, - capability_install_hint, -) -from vidxp.capabilities.registry import ( - capability_names, - collection_names, - dependency_checks, - get_capability, - index_capability_names, - preparable_capability_names, - validate_capability_options, - validate_capability_names, +from vidxp.application_boundary import application_boundary +from vidxp.application_models import ( + ApplicationError, + Artifact, + CapabilityDependencyCheck, + ComponentReadiness, + CreateIndexCommand, + CreateActorOverlayCommand, + CreateSnippetCommand, + DependencyCheckCommand, + DependencyCheckResult, + DependencyKind, + DependencyUnavailableError, + ErrorCategory, + FusedSearchResult, + IndexResult, + ImportMediaCommand, + IndexSnapshotReference, + MediaAsset, + ModelUnavailableError, + PrepareModelsCommand, + PrepareModelsResult, + RemoveIndexCommand, + ResourceNotFoundError, + RuntimeReadiness, + QueryAnswer, + QueryVideoCommand, + SearchCommand, + SearchMomentsPlanStep, ) from vidxp.capabilities.actor.schemas import ( ActorClusterSummary, - ActorDetection, - ActorRenderResult, + ActorClustersOutput, + ActorDetectionsOutput, +) +from vidxp.capabilities.contracts import ( + CapabilityDependencyError, + CapabilityContext, + CapabilityRequestError, + PreparationContext, ) +from vidxp.capabilities.registry import CapabilityRegistry +from vidxp.capability_service import CapabilityService from vidxp.capabilities.schemas import SearchResult from vidxp.core.contracts import ( - CancellationToken, IndexConfig, - IndexSchemaError, -) -from vidxp.core.manifest import ( - CHECKPOINT_DIRECTORY, - COMPLETION_FILE, - FAILURES_FILE, - MANIFEST_FILE, - TIMINGS_FILE, ) -from vidxp.core.runner import ( - ProgressCallback, - index_video, - indexing_in_progress, - local_config_from_status, +from vidxp.execution import ExecutionContext, execution_context +from vidxp.ports import IndexBackend, ModelRuntimePort, QueryModelPort +from vidxp.query_service import GroundedQueryService +from vidxp.search_fusion import fuse_search_results +from vidxp.model_contracts import ModelArtifactUnavailableError +from vidxp.repository_layout import RepositoryLayout +from vidxp.settings import VidXPSettings +from vidxp.control_plane import ControlPlaneApplication +from vidxp.artifact_service import ( + ArtifactService, ) -from vidxp.core.storage import IndexStorage -from vidxp.index_state import ( - INDEX_STATUS_FILE, - INDEX_STATUS_SCHEMA, - IndexingInProgressError, - read_index_status, - require_ready_index, +from vidxp.media_service import ( + MediaService, ) -class VidXPService: - """Reusable application boundary for CLI, HTTP, and other adapters.""" +class VidXPApplication(ControlPlaneApplication): + """The transport-neutral command and query boundary.""" def __init__( self, - index_directory: str | Path = "chroma_data", *, - device: str | None = None, + settings: VidXPSettings, + layout: RepositoryLayout, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, + index_backend: IndexBackend, + media: MediaService, + artifacts: ArtifactService, + index_status: Callable[[], dict[str, Any] | None], + completed_upload_importer: Callable[[str], MediaAsset] | None = None, + query_model: QueryModelPort | None = None, ) -> None: - self.index_directory = Path(index_directory) - self.device = device - - def index_status(self) -> dict[str, Any]: - status = read_index_status(self.index_directory) - if status is not None: - payload = dict(status) - else: - payload = { - "schema_version": INDEX_STATUS_SCHEMA, - "state": "missing", - "stage": "status", - "message": "No local video index was found.", - } - payload["index_directory"] = str(self.index_directory) - return payload + self.registry = registry + self.runtime = runtime + self.index_backend = index_backend + self.query = GroundedQueryService(query_model) + self._completed_upload_importer = completed_upload_importer + super().__init__( + layout=layout, + capabilities=CapabilityService(registry), + media=media, + artifacts=artifacts, + index_status=index_status, + model_cache=settings.model_cache, + ) + self.settings = settings - def active_config(self) -> tuple[IndexConfig, dict[str, Any]]: - status = require_ready_index(self.index_directory) - config = local_config_from_status( - status, - storage_directory=self.index_directory, + @contextmanager + def _capability_dependencies( + self, + capabilities: tuple[str, ...], + *, + missing_resource: str | None = None, + ) -> Iterator[None]: + try: + yield + except FileNotFoundError as exc: + if missing_resource is not None: + raise ResourceNotFoundError(missing_resource) from exc + raise DependencyUnavailableError( + capabilities, + self.registry.install_hint(capabilities), + ) from exc + except ModelArtifactUnavailableError as exc: + raise ModelUnavailableError(exc.capability) from exc + except (ModuleNotFoundError, CapabilityDependencyError) as exc: + raise DependencyUnavailableError( + capabilities, + self.registry.install_hint(capabilities), + ) from exc + + @property + def index_directory(self) -> Path: + return self.layout.local_index + + @property + def device(self) -> str: + return self.runtime.backends.torch_device + + @application_boundary + def _active_config(self) -> IndexConfig: + return self.index_backend.active_config( + self.index_directory, + device=self.device, + ) + + @application_boundary + def import_media(self, command: ImportMediaCommand) -> MediaAsset: + self.layout.ensure_local_directories() + return self.media.import_local(command) + + @application_boundary + def import_completed_upload(self, upload_id: str) -> MediaAsset: + if self._completed_upload_importer is None: + raise ApplicationError( + "remote_upload_unavailable", + ErrorCategory.unavailable, + "Remote resumable uploads are not configured.", + ) + return self._completed_upload_importer(upload_id) + + @application_boundary + def runtime_readiness(self) -> RuntimeReadiness: + components = ( + *self.control_plane_readiness(), + self._index_storage_readiness(), + ) + dependencies = self.check_dependencies( + DependencyCheckCommand( + modalities=self.registry.names(), + include_models=True, + ) + ) + return RuntimeReadiness( + ready=( + all(component.ready for component in components) + and dependencies.ok + ), + runtime=self.runtime.backends, + components=components, + dependencies=dependencies, ) - if self.device is not None: - config = replace(config, device=self.device) - return config, status + def _index_storage_readiness(self) -> ComponentReadiness: + try: + self.index_backend.status(self.index_directory) + except Exception: + return ComponentReadiness( + name="index_storage", + ready=False, + message="The committed index storage failed integrity checks.", + ) + return ComponentReadiness( + name="index_storage", + ready=True, + message="The committed index storage passed integrity checks.", + ) + + @application_boundary def create_index( self, - video_path: str | Path, + command: CreateIndexCommand, *, - modalities: Iterable[str] | None = None, - frame_stride: int = 1, - capability_options: Mapping[ - str, - Mapping[str, Any], - ] | None = None, - progress_callback: ProgressCallback | None = None, - cancellation: CancellationToken | None = None, - source_name: str | None = None, - ) -> dict[str, Any]: - selected = self._validate_modalities( - index_capability_names() if modalities is None else modalities - ) + execution: ExecutionContext | None = None, + ) -> IndexResult: + active_execution = execution_context(execution) + selected = self.registry.validate_names(command.modalities) non_indexable = [ name for name in selected - if get_capability(name).indexer is None + if self.registry.get(name).collection_name is None ] if non_indexable: - raise ValueError( - "These capabilities do not support indexing: " - + ", ".join(non_indexable) + raise CapabilityRequestError( + "One or more selected capabilities do not support indexing." ) - options: dict[str, Any] = { - "enabled_modalities": selected, - "frame_stride": frame_stride, - "storage_directory": self.index_directory, - "collection_names": collection_names(selected), - "capability_options": validate_capability_options( + media = self.media.require_record(command.media_id) + content = self.media.content(command.media_id) + self.layout.ensure_local_directories() + capability_options = { + name: dict(options) + for name, options in command.capability_options.items() + } + if command.scene_sample_fps is not None: + capability_options.setdefault("scene", {})["sample_fps"] = ( + command.scene_sample_fps + ) + config = IndexConfig.local( + video_id=command.media_id, + enabled_modalities=selected, + frame_stride=command.frame_stride, + storage_directory=self.index_directory, + collection_names=self.registry.collection_names(selected), + capability_options=self.registry.validate_options( selected, capability_options, ), - } - if self.device is not None: - options["device"] = self.device - config = IndexConfig.local(**options) - return index_video( - str(video_path), - progress_callback=progress_callback, - source_name=source_name, - config=config, - cancellation=cancellation, + device=self.device, ) + with self.runtime.scheduler.indexing(): + with self._capability_dependencies(selected): + result = self.index_backend.create( + content.path, + config=config, + progress=active_execution.report, + cancellation=active_execution.cancellation, + operation_id=active_execution.operation_id, + source_name=media.original_filename, + source_checksum=media.sha256, + ) + return IndexResult.model_validate(result) + @application_boundary def indexing_in_progress(self) -> bool: - options: dict[str, Any] = { - "storage_directory": self.index_directory, - } - if self.device is not None: - options["device"] = self.device - return indexing_in_progress(IndexConfig.local(**options)) + return self.index_backend.indexing_in_progress( + self._base_config() + ) + @application_boundary def check_dependencies( self, - modalities: Iterable[str] | None = None, - ) -> dict[str, Any]: - selected = self._validate_modalities( - capability_names() if modalities is None else modalities - ) - checks = list(dependency_checks(selected)) - return { - "ok": all(check["ok"] for check in checks), - "modalities": list(selected), - "checks": checks, - } + command: DependencyCheckCommand, + *, + on_check_start: ( + Callable[[str, DependencyKind, str], None] | None + ) = None, + on_check_complete: ( + Callable[[CapabilityDependencyCheck, float], None] | None + ) = None, + ) -> DependencyCheckResult: + selected = self.registry.validate_names(command.modalities) + checks = ( + *self.registry.dependency_checks( + selected, + include_runtime_checks=command.include_runtime_checks, + on_check_start=on_check_start, + on_check_complete=on_check_complete, + ), + *( + self.registry.model_checks( + selected, + cache=self.settings.model_cache, + on_check_start=on_check_start, + on_check_complete=on_check_complete, + ) + if command.include_models + else () + ), + *( + self._media_runtime_checks( + on_check_start=on_check_start, + on_check_complete=on_check_complete, + ) + if command.include_runtime_checks + else () + ), + ) + return DependencyCheckResult( + ok=all(check.ok for check in checks), + modalities=selected, + checks=checks, + ) + def _media_runtime_checks( + self, + *, + on_check_start: ( + Callable[[str, DependencyKind, str], None] | None + ) = None, + on_check_complete: ( + Callable[[CapabilityDependencyCheck, float], None] | None + ) = None, + ) -> tuple[CapabilityDependencyCheck, ...]: + checks = [] + for name, executable, setting in ( + ( + "ffmpeg", + self.settings.ffmpeg_executable, + "VIDXP_FFMPEG_EXECUTABLE", + ), + ( + "ffprobe", + self.settings.ffprobe_executable, + "VIDXP_FFPROBE_EXECUTABLE", + ), + ): + if on_check_start is not None: + on_check_start("media", DependencyKind.runtime, name) + started = perf_counter() + resolved = which(executable) + check = CapabilityDependencyCheck( + capability="media", + kind=DependencyKind.runtime, + name=name, + ok=resolved is not None, + error=( + None + if resolved is not None + else ( + f"{executable!r} is unavailable. Install FFmpeg " + "with the operating-system package manager or set " + f"{setting}; VidXP does not install OS packages." + ) + ), + ) + checks.append(check) + if on_check_complete is not None: + on_check_complete( + check, + perf_counter() - started, + ) + return tuple(checks) + + @application_boundary def prepare_models( self, - modalities: Iterable[str] | None = None, + command: PrepareModelsCommand, *, - capability_options: Mapping[ - str, - Mapping[str, Any], - ] | None = None, - progress_callback: ProgressCallback | None = None, - ) -> dict[str, Any]: - selected = self._validate_modalities( - preparable_capability_names() - if modalities is None - else modalities - ) - options = validate_capability_options(selected, capability_options) - checks = dependency_checks(selected) - failures = [check for check in checks if not check["ok"]] + execution: ExecutionContext | None = None, + ) -> PrepareModelsResult: + active_execution = execution_context(execution) + selected = self.registry.validate_names(command.modalities) + options = self.registry.validate_options( + selected, + command.capability_options, + ) + checks = self.registry.dependency_checks(selected) + failures = [check for check in checks if not check.ok] if failures: - details = "; ".join( - f"{check['name']}: {check['error']}" - for check in failures - ) - extras = ",".join( - get_capability(name).extra for name in selected - ) - raise RuntimeError( - f"{details}. {capability_install_hint(extras)}" + raise DependencyUnavailableError( + selected, + self.registry.install_hint(selected), ) - prepared = [] - for name in selected: - capability = get_capability(name) - prepare = capability.prepare - if prepare is not None: - prepared.extend( - prepare( - PreparationContext( - device=self.device or "cpu", - settings=capability.config_model.model_validate( - options[name] - ), - ), - progress_callback, - ) - ) - return { - "prepared": prepared, - "modalities": list(selected), - "device": self.device or "cpu", - } + prepared: list[str] = [] + with self.runtime.scheduler.inference(): + with self._capability_dependencies(selected): + for name in selected: + active_execution.checkpoint() + executor = self.registry.executor(name) + if executor.prepare is not None: + prepared.extend( + executor.prepare( + PreparationContext( + runtime=self.runtime, + settings=self.registry.get(name) + .config_model.model_validate(options[name]), + ), + active_execution.report, + ) + ) + active_execution.checkpoint() + return PrepareModelsResult( + prepared=tuple(prepared), + modalities=selected, + runtime=self.runtime.backends, + ) + @application_boundary def execute( self, capability: str, operation: str, payload: BaseModel | Mapping[str, Any], ) -> BaseModel: - """Validate and execute a registered capability operation.""" + definition, contract, handler = self._operation( + capability, + operation, + ) + request = contract.input_model.model_validate(payload) + config = None + if contract.requires_index: + config = self._active_config() + self._require_indexed_capability(capability, config) + + with self._capability_dependencies((capability,)): + if config is None: + with self.runtime.scheduler.inference(): + response = handler( + CapabilityContext( + config=None, + runtime=self.runtime, + ), + request, + ) + return contract.output_model.model_validate(response) + else: + with self.index_backend.open_store(config) as storage: + with self.runtime.scheduler.inference(): + response = handler( + CapabilityContext( + config=config, + runtime=self.runtime, + storage=storage, + ), + request, + ) + return contract.output_model.model_validate(response) - definition = get_capability(capability) + def _operation(self, capability: str, operation: str): + definition = self.registry.get(capability) try: - selected_operation = definition.operations[operation] + contract = definition.operations[operation] + handler = self.registry.executor(capability).operations[operation] except KeyError as exc: available = ", ".join(definition.operations) or "none" - raise ValueError( - f"Capability {capability!r} has no operation {operation!r}. " - f"Available operations: {available}." + raise CapabilityRequestError( + f"Capability {capability!r} has no operation {operation!r}; " + f"available operations: {available}." ) from exc - config = None - if selected_operation.requires_index: - config, _ = self.active_config() - if capability not in config.enabled_modalities: - raise ValueError( - f"The {capability} capability is not present in this index." - ) - try: - return selected_operation.invoke( - CapabilityContext(config=config), - payload, + return definition, contract, handler + + @staticmethod + def _require_indexed_capability( + capability: str, + config: IndexConfig, + ) -> None: + if capability not in config.enabled_modalities: + raise CapabilityRequestError( + f"The {capability} capability is not present in this index." ) - except ModuleNotFoundError as exc: - dependency = exc.name or "optional dependency" - raise RuntimeError( - f"{dependency} is unavailable. " - + capability_install_hint(definition.extra) - ) from exc + def _invoke_operation( + self, + capability: str, + operation: str, + payload: BaseModel | Mapping[str, Any], + *, + context: CapabilityContext, + ) -> BaseModel: + _, contract, handler = self._operation(capability, operation) + if contract.requires_index: + if context.config is None or context.storage is None: + raise RuntimeError( + "Indexed capability execution requires a pinned store." + ) + self._require_indexed_capability(capability, context.config) + request = contract.input_model.model_validate(payload) + response = handler(context, request) + return contract.output_model.model_validate(response) + + def _config_for_snapshot( + self, + reference: IndexSnapshotReference, + ) -> IndexConfig: + return self.index_backend.config_for_snapshot( + self.index_directory, + snapshot_id=reference.snapshot_id, + snapshot_sha256=reference.snapshot_sha256, + device=self.device, + ) + + @application_boundary def search( + self, + command: SearchCommand, + *, + snapshot: IndexSnapshotReference | None = None, + ) -> FusedSearchResult: + config = ( + self._config_for_snapshot(snapshot) + if snapshot is not None + else self._active_config() + ) + selected, _ = self._resolve_query_capabilities( + command.modalities, + config, + include_actor=False, + ) + with self._capability_dependencies(selected): + with self.index_backend.open_store(config) as storage: + context = CapabilityContext( + config=config, + runtime=self.runtime, + storage=storage, + ) + with self.runtime.scheduler.inference(): + results = tuple( + self._search_capability( + modality, + query=command.query, + media_id=command.media_id, + top_k=command.top_k, + context=context, + ) + for modality in selected + ) + return fuse_search_results( + query=command.query, + requested_modalities=selected, + results=results, + media_id=command.media_id, + top_k=command.top_k, + ) + + def _resolve_query_capabilities( + self, + requested: tuple[str, ...], + config: IndexConfig, + *, + include_actor: bool, + ) -> tuple[tuple[str, ...], bool]: + explicit = bool(requested) + candidates = ( + self.registry.validate_names(requested) + if explicit + else tuple(config.enabled_modalities) + ) + unavailable = tuple( + name + for name in candidates + if name not in config.enabled_modalities + ) + if unavailable: + raise CapabilityRequestError( + "Query capabilities are not present in the pinned index: " + + ", ".join(unavailable) + + "." + ) + + searchable = tuple( + name + for name in candidates + if "search" in self.registry.get(name).operations + ) + actor_overview = ( + include_actor + and "actor" in candidates + and "clusters" in self.registry.get("actor").operations + ) + supported = set(searchable) + if actor_overview: + supported.add("actor") + unsupported = tuple( + name for name in candidates if name not in supported + ) + if explicit and unsupported: + operation = "Query" if include_actor else "Search" + raise CapabilityRequestError( + f"{operation} does not support these indexed capabilities: " + + ", ".join(unsupported) + + "." + ) + if not searchable and not actor_overview: + raise CapabilityRequestError( + "The pinned index has no queryable capabilities." + ) + return searchable, actor_overview + + def _search_capability( self, modality: str, - query: str, *, - top_k: int = 10, + query: str, + media_id: str | None, + top_k: int, + context: CapabilityContext, ) -> SearchResult: return cast( SearchResult, - self.execute( + self._invoke_operation( modality, "search", - {"query": query, "top_k": top_k}, + { + "query": query, + "media_id": media_id, + "top_k": top_k, + }, + context=context, ), ) - def actor_clusters(self) -> tuple[ActorClusterSummary, ...]: - result = self.execute("actor", "clusters", {}) - return tuple(result.clusters) + @application_boundary + def query_video( + self, + command: QueryVideoCommand, + *, + snapshot: IndexSnapshotReference, + execution: ExecutionContext | None = None, + ) -> QueryAnswer: + active_execution = execution_context(execution) + config = self._config_for_snapshot(snapshot) + search_modalities, actor_overview = ( + self._resolve_query_capabilities( + command.modalities, + config, + include_actor=True, + ) + ) + if len(search_modalities) + int(actor_overview) > 8: + raise CapabilityRequestError( + "Query supports at most eight capability operations." + ) - def actor_detections( + active_execution.checkpoint() + plan, planning_fallback = self.query.plan( + command, + search_modalities=search_modalities, + actor_overview=actor_overview, + ) + active_execution.checkpoint() + results: list[SearchResult] = [] + actors: tuple[ActorClusterSummary, ...] = () + dependencies = search_modalities + ( + ("actor",) if actor_overview else () + ) + with self._capability_dependencies(dependencies): + with self.index_backend.open_store(config) as storage: + context = CapabilityContext( + config=config, + runtime=self.runtime, + storage=storage, + ) + with self.runtime.scheduler.inference(): + for step in plan.steps: + active_execution.checkpoint() + if isinstance(step, SearchMomentsPlanStep): + results.append( + self._search_capability( + step.modality, + query=step.query, + media_id=command.media_id, + top_k=command.top_k, + context=context, + ) + ) + else: + page = cast( + ActorClustersOutput, + self._invoke_operation( + "actor", + "clusters", + { + "page_size": min(command.top_k, 100), + "media_id": command.media_id, + }, + context=context, + ), + ) + actors = page.clusters + active_execution.checkpoint() + atomic = tuple(results) + fused = fuse_search_results( + query=command.question, + requested_modalities=search_modalities, + results=atomic, + media_id=command.media_id, + top_k=command.top_k, + ) + evidence = self.query.evidence( + snapshot=snapshot, + fused=fused, + actors=actors, + ) + active_execution.checkpoint() + answer = self.query.answer( + command, + plan=plan, + planning_fallback=planning_fallback, + evidence=evidence, + fused=fused, + ) + active_execution.checkpoint() + return answer + + @application_boundary + def actor_clusters( self, - cluster_id: str, - ) -> list[ActorDetection]: - result = self.execute( - "actor", - "detections", - {"cluster_id": cluster_id}, + *, + page_size: int = 50, + cursor: str | None = None, + media_id: str | None = None, + ) -> ActorClustersOutput: + return cast( + ActorClustersOutput, + self.execute( + "actor", + "clusters", + { + "page_size": page_size, + "cursor": cursor, + "media_id": media_id, + }, + ), ) - return list(result.detections) - def render_actor( + @application_boundary + def actor_cluster(self, cluster_id: str) -> ActorClusterSummary: + return cast( + ActorClusterSummary, + self.execute( + "actor", + "cluster", + {"cluster_id": cluster_id}, + ), + ) + + @application_boundary + def actor_detections( self, cluster_id: str, - input_path: str | Path, - output_path: str | Path, - ) -> ActorRenderResult: + *, + page_size: int = 50, + cursor: str | None = None, + ) -> ActorDetectionsOutput: return cast( - ActorRenderResult, + ActorDetectionsOutput, self.execute( "actor", - "render", + "detections", { "cluster_id": cluster_id, - "input_path": input_path, - "output_path": output_path, + "page_size": page_size, + "cursor": cursor, }, ), ) - def clear_index(self) -> bool: - if not self.index_directory.exists(): - return False - base_config = IndexConfig.local( - storage_directory=self.index_directory, - enabled_modalities=index_capability_names(), - collection_names=collection_names(), + @application_boundary + def render_actor( + self, + command: CreateActorOverlayCommand, + *, + snapshot: IndexSnapshotReference | None = None, + media_id: str | None = None, + generation_id: str | None = None, + execution: ExecutionContext | None = None, + ) -> Artifact: + active_execution = execution_context(execution) + if (media_id is None) != (generation_id is None): + raise ValueError( + "Pinned actor media and generation identities must be " + "provided together." + ) + active_execution.report( + { + "stage": "resolving_detections", + "message": "Resolving actor detections.", + } + ) + config = ( + self._config_for_snapshot(snapshot) + if snapshot is not None + else self._active_config() ) - if indexing_in_progress(base_config): - raise IndexingInProgressError( - f"Indexing is active for {self.index_directory}." + self._require_indexed_capability("actor", config) + detections = [] + with self._capability_dependencies(("actor",)): + with self.index_backend.open_store(config) as storage: + context = CapabilityContext( + config=config, + runtime=self.runtime, + storage=storage, + ) + with self.runtime.scheduler.inference(): + cluster = cast( + ActorClusterSummary, + self._invoke_operation( + "actor", + "cluster", + {"cluster_id": command.cluster_id}, + context=context, + ), + ) + cursor = None + while True: + active_execution.checkpoint() + page = cast( + ActorDetectionsOutput, + self._invoke_operation( + "actor", + "detections", + { + "cluster_id": command.cluster_id, + "page_size": 100, + "cursor": cursor, + }, + context=context, + ), + ) + detections.extend(page.detections) + cursor = page.next_cursor + if cursor is None: + break + identities = { + (detection.media_id, detection.generation_id) + for detection in detections + } + expected_identity = (cluster.media_id, cluster.generation_id) + if ( + identities != {expected_identity} + or ( + media_id is not None + and expected_identity != (media_id, generation_id) ) + ): + raise ApplicationError( + "actor_cluster_identity_invalid", + ErrorCategory.conflict, + "The actor cluster does not match the requested index identity.", + ) + return self.artifacts.create_actor_overlay( + media_id=cluster.media_id, + generation_id=cluster.generation_id, + cluster_id=command.cluster_id, + detections=[ + detection.model_dump(mode="python") + for detection in detections + ], + profile=command.profile, + job_id=active_execution.job_id, + execution=active_execution, + ) - status = read_index_status(self.index_directory) - if status is not None and status.get("state") == "ready": - try: - config = local_config_from_status( - status, - storage_directory=self.index_directory, - ) - except (IndexSchemaError, KeyError, TypeError, ValueError): - config = base_config - else: - config = base_config + @application_boundary + def create_snippet( + self, + command: CreateSnippetCommand, + *, + execution: ExecutionContext | None = None, + ) -> Artifact: + active_execution = execution_context(execution) + return self.artifacts.create_snippet( + command, + job_id=active_execution.job_id, + execution=active_execution, + ) + + @application_boundary + def clear_index(self) -> bool: + base_config = self._base_config() try: - with IndexStorage(config) as storage: - storage.clear() + return self.index_backend.clear(base_config) except ModuleNotFoundError as exc: - dependency = exc.name or "optional storage dependency" - raise RuntimeError( - f"{dependency} is unavailable. " - + capability_install_hint("storage") + raise DependencyUnavailableError( + self.registry.index_names(), + self.registry.install_hint(self.registry.index_names()), ) from exc - for name in ( - INDEX_STATUS_FILE, - MANIFEST_FILE, - TIMINGS_FILE, - FAILURES_FILE, - COMPLETION_FILE, - ): - (self.index_directory / name).unlink(missing_ok=True) - checkpoint_directory = self.index_directory / CHECKPOINT_DIRECTORY - if checkpoint_directory.is_dir(): - for checkpoint in checkpoint_directory.glob("*.json"): - checkpoint.unlink() - try: - checkpoint_directory.rmdir() - except OSError: - pass - return True + @application_boundary + def remove_from_index(self, command: RemoveIndexCommand) -> bool: + return self.index_backend.remove( + self._base_config(), + command.media_id, + ) - @staticmethod - def _validate_modalities( - modalities: Iterable[str], - ) -> tuple[str, ...]: - return validate_capability_names(modalities) + def _base_config(self) -> IndexConfig: + return IndexConfig.local( + storage_directory=self.index_directory, + enabled_modalities=self.registry.index_names(), + collection_names=self.registry.collection_names(), + device=self.device, + ) diff --git a/src/vidxp/application_boundary.py b/src/vidxp/application_boundary.py new file mode 100644 index 0000000..33caa4a --- /dev/null +++ b/src/vidxp/application_boundary.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +from functools import wraps +from typing import Any, Callable + +from pydantic import JsonValue, ValidationError + +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + InvalidRequestError, + ResourceNotFoundError, +) +from vidxp.artifact_service import ( + ArtifactRequestError, + ArtifactUnavailableError, + InvalidArtifactError, +) +from vidxp.capabilities.actor.results import ActorClusterNotFoundError +from vidxp.capabilities.contracts import CapabilityRequestError +from vidxp.core.artifacts import ( + ArtifactIntegrityError, + ArtifactRenderError, + ArtifactRendererUnavailableError, +) +from vidxp.core.contracts import ( + IndexCancelledError, + IndexSchemaError, +) +from vidxp.core.media import ( + InvalidMediaError, + MediaImportLimitError, + MediaProbeUnavailableError, + MediaStoreIntegrityError, + MediaUnavailableError, +) +from vidxp.core.storage import IndexStorageUnavailableError +from vidxp.index_state import ( + IndexingInProgressError, + IndexNotReadyError, +) +from vidxp.media_service import ( + MediaIdempotencyConflictError, + MediaImportNotAllowedError, +) +def _validation_details( + exc: ValidationError, +) -> list[dict[str, JsonValue]]: + return [ + { + "type": item["type"], + "location": [str(part) for part in item["loc"]], + "message": item["msg"], + } + for item in exc.errors( + include_url=False, + include_context=False, + include_input=False, + ) + ] + + +def application_boundary(handler: Callable) -> Callable: + """Translate expected application failures once for every transport.""" + + @wraps(handler) + def wrapped(*args: Any, **kwargs: Any) -> Any: + try: + return handler(*args, **kwargs) + except ApplicationError: + raise + except ActorClusterNotFoundError as exc: + raise ApplicationError( + "actor_cluster_not_found", + ErrorCategory.not_found, + "The requested actor cluster was not found.", + ) from exc + except IndexingInProgressError as exc: + raise ApplicationError( + "indexing_in_progress", + ErrorCategory.conflict, + "An indexing operation is already in progress.", + retryable=True, + ) from exc + except IndexNotReadyError as exc: + raise ApplicationError( + "index_not_ready", + ErrorCategory.conflict, + "The index is not ready.", + retryable=True, + ) from exc + except IndexSchemaError as exc: + raise ApplicationError( + "index_schema_incompatible", + ErrorCategory.conflict, + "The index schema is incompatible with this version.", + ) from exc + except IndexStorageUnavailableError as exc: + raise ApplicationError( + "index_storage_unavailable", + ErrorCategory.unavailable, + "The remote index storage is unavailable.", + retryable=True, + ) from exc + except IndexCancelledError as exc: + raise ApplicationError( + "operation_cancelled", + ErrorCategory.cancelled, + "The operation was cancelled.", + ) from exc + except ValidationError as exc: + raise InvalidRequestError( + errors=_validation_details(exc), + ) from exc + except CapabilityRequestError as exc: + raise InvalidRequestError() from exc + except InvalidMediaError as exc: + raise ApplicationError( + "media_invalid", + ErrorCategory.validation, + "The selected file is not a valid supported video.", + ) from exc + except MediaImportLimitError as exc: + raise ApplicationError( + "media_too_large", + ErrorCategory.resource_limit, + "The selected media exceeds the configured import limit.", + ) from exc + except MediaProbeUnavailableError as exc: + raise ApplicationError( + "media_probe_unavailable", + ErrorCategory.unavailable, + "The media probe dependency is unavailable. " + "For a local installation, run `vidxp init` and retry.", + details={"remediation": "vidxp init"}, + retryable=True, + ) from exc + except MediaImportNotAllowedError as exc: + raise ApplicationError( + "media_import_forbidden", + ErrorCategory.authorization, + "The selected local media cannot be imported.", + ) from exc + except MediaIdempotencyConflictError as exc: + raise ApplicationError( + "idempotency_key_reused", + ErrorCategory.validation, + "The idempotency key was already used for other media.", + ) from exc + except MediaStoreIntegrityError as exc: + raise ApplicationError( + "media_integrity_failed", + ErrorCategory.conflict, + "The media changed or failed an integrity check.", + ) from exc + except MediaUnavailableError as exc: + raise ResourceNotFoundError("media") from exc + except ArtifactUnavailableError as exc: + raise ResourceNotFoundError("artifact") from exc + except ArtifactIntegrityError as exc: + raise ApplicationError( + "artifact_integrity_failed", + ErrorCategory.conflict, + "The artifact failed an integrity check.", + ) from exc + except ArtifactRendererUnavailableError as exc: + raise ApplicationError( + "artifact_renderer_unavailable", + ErrorCategory.unavailable, + "The configured artifact renderer is unavailable. " + "For a local installation, run `vidxp init` and retry.", + details={"remediation": "vidxp init"}, + retryable=True, + ) from exc + except ArtifactRenderError as exc: + raise ApplicationError( + "artifact_render_failed", + ErrorCategory.internal, + "The requested artifact could not be rendered.", + ) from exc + except ArtifactRequestError as exc: + raise InvalidRequestError() from exc + except InvalidArtifactError as exc: + raise ApplicationError( + "artifact_render_invalid", + ErrorCategory.internal, + "The generated artifact failed media validation.", + ) from exc + + return wrapped diff --git a/src/vidxp/application_models.py b/src/vidxp/application_models.py new file mode 100644 index 0000000..9975e3a --- /dev/null +++ b/src/vidxp/application_models.py @@ -0,0 +1,1081 @@ +from __future__ import annotations + +from enum import StrEnum +from pathlib import Path +from typing import Annotated, Any, Generic, Literal, Mapping, TypeAlias, TypeVar + +from pydantic import ( + AwareDatetime, + BaseModel, + ConfigDict, + Field, + JsonValue, + NonNegativeInt, + StringConstraints, + field_validator, + model_validator, +) + +from vidxp.core.identifiers import ( + ActorClusterId as ActorClusterId, + ArtifactId as ArtifactId, + Identifier as Identifier, + IndexGenerationId as IndexGenerationId, + IndexSnapshotId as IndexSnapshotId, + JobId as JobId, + MediaId as MediaId, + MimeType, + Sha256, + UploadIntentId as UploadIntentId, + VideoId as VideoId, +) +from vidxp.core.artifacts import ( + ARTIFACT_SCHEMA_VERSION, + ArtifactKind, + ArtifactState, +) +from vidxp.core.contracts import INDEX_SCHEMA_VERSION +from vidxp.core.media import ( + MEDIA_SCHEMA_VERSION, + MediaState, + MediaStream, + validate_display_filename, +) +from vidxp.core.uploads import UploadState +from vidxp.index_state import INDEX_STATUS_MEDIA_ID_LIMIT + +T = TypeVar("T") +SearchQuery: TypeAlias = Annotated[ + str, + StringConstraints(strip_whitespace=True, min_length=1, max_length=4096), +] + + +class ApplicationModel(BaseModel): + model_config = ConfigDict( + extra="forbid", + frozen=True, + allow_inf_nan=False, + ) + + +class Page(ApplicationModel, Generic[T]): + items: tuple[T, ...] = () + total: int = Field(ge=0) + next_cursor: str | None = None + + +class RuntimeProfile(ApplicationModel): + requested: str + torch_device: str + transcription_device: str + actor_device: str = "cpu" + mps_available: bool = False + cuda_available: bool = False + + +class Principal(ApplicationModel): + subject: str = Field(min_length=1, max_length=255) + client_id: str | None = Field(default=None, min_length=1, max_length=255) + scopes: frozenset[str] = Field(default_factory=frozenset) + + +class ErrorCategory(StrEnum): + validation = "validation" + authentication = "authentication" + authorization = "authorization" + not_found = "not_found" + conflict = "conflict" + unavailable = "unavailable" + resource_limit = "resource_limit" + cancelled = "cancelled" + internal = "internal" + + +class JobKind(StrEnum): + media_import = "media_import" + index = "index" + search = "search" + query = "query" + snippet = "snippet" + actor_overlay = "actor_overlay" + prepare_models = "prepare_models" + + +class JobState(StrEnum): + queued = "queued" + running = "running" + succeeded = "succeeded" + failed = "failed" + cancelled = "cancelled" + recovery_exhausted = "recovery_exhausted" + + +class JobQueue(StrEnum): + cpu = "cpu" + gpu = "gpu" + + +class ErrorDetail(ApplicationModel): + code: str = Field(min_length=1, pattern=r"^[a-z][a-z0-9_]*$") + category: ErrorCategory + message: str = Field(min_length=1) + details: dict[str, JsonValue] = Field(default_factory=dict) + retryable: bool = False + correlation_id: str | None = None + + +class ApplicationError(RuntimeError): + def __init__( + self, + code: str, + category: ErrorCategory, + message: str, + *, + details: Mapping[str, Any] | None = None, + retryable: bool = False, + correlation_id: str | None = None, + ) -> None: + self.detail = ErrorDetail( + code=code, + category=category, + message=message, + details=details or {}, + retryable=retryable, + correlation_id=correlation_id, + ) + super().__init__(message) + + @property + def code(self) -> str: + return self.detail.code + + @property + def category(self) -> ErrorCategory: + return self.detail.category + + @property + def retryable(self) -> bool: + return self.detail.retryable + + def to_dict(self) -> dict[str, Any]: + return self.detail.model_dump(mode="json") + + +class DependencyUnavailableError(ApplicationError): + def __init__( + self, + capabilities: tuple[str, ...], + install_hint: str, + ) -> None: + label = capabilities[0] if len(capabilities) == 1 else "selected" + super().__init__( + "dependency_unavailable", + ErrorCategory.unavailable, + f"Dependencies for the {label} capability are unavailable. " + f"{install_hint}", + details={ + "capabilities": list(capabilities), + "install_hint": install_hint, + }, + ) + + +class ModelUnavailableError(ApplicationError): + def __init__(self, capability: str) -> None: + modality = capability.split(".", 1)[0] + super().__init__( + "model_unavailable", + ErrorCategory.unavailable, + f"Model artifacts for the {capability} capability are not " + "available locally. Run " + f"`vidxp prepare --modalities {modality}` before retrying.", + details={ + "capability": capability, + "remediation": ( + f"vidxp prepare --modalities {modality}" + ), + }, + ) + + +class InvalidRequestError(ApplicationError): + def __init__( + self, + *, + errors: list[dict[str, JsonValue]] | None = None, + ) -> None: + super().__init__( + "invalid_request", + ErrorCategory.validation, + "The request is invalid.", + details={"errors": errors or []}, + ) + + +class ResourceNotFoundError(ApplicationError): + def __init__(self, resource: str) -> None: + super().__init__( + "resource_not_found", + ErrorCategory.not_found, + f"The requested {resource} was not found.", + details={"resource": resource}, + ) + + +class CapabilityProvenance(ApplicationModel): + distribution: str = Field(min_length=1) + entry_point: str = Field(min_length=1) + version: str | None = None + + +class DependencyKind(StrEnum): + distribution = "distribution" + runtime = "runtime" + model = "model" + + +class CapabilityDependencyCheck(ApplicationModel): + capability: str = Field(min_length=1) + provenance: CapabilityProvenance | None = None + kind: DependencyKind + name: str = Field(min_length=1) + requirement: str | None = None + installed_version: str | None = None + download_size_bytes: int | None = Field(default=None, ge=1) + ok: bool + error: str | None = None + + +class CapabilityOperationInfo(ApplicationModel): + name: str = Field(min_length=1) + requires_index: bool + input_schema: dict[str, JsonValue] + output_schema: dict[str, JsonValue] + + +class CapabilitySummary(ApplicationModel): + name: str = Field(min_length=1) + description: str = Field(min_length=1) + install_extra: str = Field(min_length=1) + supports_indexing: bool + prepares_models: bool + provenance: CapabilityProvenance | None = None + + +class CapabilityInfo(CapabilitySummary): + operations: tuple[CapabilityOperationInfo, ...] = () + + +class CapabilityList(ApplicationModel): + items: tuple[CapabilitySummary, ...] = () + + +class ComponentReadiness(ApplicationModel): + name: str = Field(min_length=1) + ready: bool + message: str = Field(min_length=1) + + +class RuntimeReadiness(ApplicationModel): + ready: bool + runtime: RuntimeProfile | None + components: tuple[ComponentReadiness, ...] + dependencies: DependencyCheckResult | None + + +class ImportMediaCommand(ApplicationModel): + """Local-adapter command; remote ingestion uses the upload workflow.""" + + path: Path + original_filename: str | None = Field(default=None, min_length=1) + declared_mime_type: MimeType | None = None + + @field_validator("original_filename") + @classmethod + def _filename_only(cls, value: str | None) -> str | None: + return None if value is None else validate_display_filename(value) + + +class MediaAsset(ApplicationModel): + schema_version: Literal[MEDIA_SCHEMA_VERSION] = MEDIA_SCHEMA_VERSION + media_id: MediaId = Field( + description=( + "Stable registered-media identifier used by indexing and optional " + "single-media search/query filters." + ) + ) + video_id: VideoId + original_filename: str = Field(min_length=1) + sha256: Sha256 + byte_size: int = Field(gt=0) + declared_mime_type: MimeType | None = None + detected_mime_type: MimeType + container: str = Field(min_length=1) + duration_seconds: float = Field(gt=0) + streams: tuple[MediaStream, ...] = Field(min_length=1) + state: MediaState + created_at: AwareDatetime + + +class ListMediaCommand(ApplicationModel): + page_size: int = Field( + default=50, + gt=0, + le=100, + description="Maximum registered media records to return.", + ) + cursor: str | None = Field( + default=None, + min_length=1, + max_length=512, + description="Opaque next_cursor from the previous list_media page.", + ) + + +class MediaPage(Page[MediaAsset]): + pass + + +class CreateUploadIntentCommand(ApplicationModel): + original_filename: str = Field(min_length=1, max_length=255) + byte_size: int = Field(gt=0) + declared_mime_type: MimeType | None = None + + @field_validator("original_filename") + @classmethod + def _filename_only(cls, value: str) -> str: + return validate_display_filename(value) + + +class UploadIntent(ApplicationModel): + intent_id: UploadIntentId + original_filename: str = Field(min_length=1, max_length=255) + byte_size: int = Field(gt=0) + declared_mime_type: MimeType | None = None + state: UploadState + created_at: AwareDatetime + expires_at: AwareDatetime + job_id: JobId | None = None + media_id: MediaId | None = None + + +class CreateIndexCommand(ApplicationModel): + media_id: MediaId = Field( + description=( + "Stable identifier returned by list_media, get_media, or a " + "completed upload." + ) + ) + modalities: tuple[str, ...] + frame_stride: int = Field( + default=1, + gt=0, + description=( + "Materialize every Nth frame for actor and legacy visual indexing." + ), + ) + scene_sample_fps: float | None = Field( + default=None, + gt=0, + description=( + "Target scene samples per second. Sources below this rate use " + "every available frame without duplication." + ), + ) + capability_options: Mapping[str, Mapping[str, JsonValue]] = Field( + default_factory=dict + ) + + @model_validator(mode="before") + @classmethod + def _canonicalize_scene_sampling(cls, value: Any) -> Any: + if not isinstance(value, Mapping): + return value + payload = dict(value) + raw_options = payload.get("capability_options") + if not isinstance(raw_options, Mapping): + return payload + raw_scene = raw_options.get("scene") + if not isinstance(raw_scene, Mapping) or "sample_fps" not in raw_scene: + return payload + + nested_value = raw_scene["sample_fps"] + explicit_value = payload.get("scene_sample_fps") + if explicit_value is not None: + try: + conflicts = float(explicit_value) != float(nested_value) + except (TypeError, ValueError): + conflicts = True + if conflicts: + raise ValueError( + "scene_sample_fps conflicts with " + "capability_options.scene.sample_fps." + ) + else: + payload["scene_sample_fps"] = nested_value + + options = dict(raw_options) + scene = dict(raw_scene) + scene.pop("sample_fps") + if scene: + options["scene"] = scene + else: + options.pop("scene", None) + payload["capability_options"] = options + return payload + + @model_validator(mode="after") + def _scene_sampling_requires_scene(self) -> "CreateIndexCommand": + if ( + self.scene_sample_fps is not None + and "scene" not in self.modalities + ): + raise ValueError( + "scene_sample_fps requires the scene modality." + ) + return self + + +class IndexResult(ApplicationModel): + media_id: MediaId + generation_id: IndexGenerationId + snapshot_id: IndexSnapshotId + active_media_count: int = Field(gt=0) + record_counts: dict[str, NonNegativeInt] = Field(default_factory=dict) + + +class RemoveIndexCommand(ApplicationModel): + media_id: MediaId + + +class Artifact(ApplicationModel): + schema_version: Literal[ARTIFACT_SCHEMA_VERSION] = ( + ARTIFACT_SCHEMA_VERSION + ) + artifact_id: ArtifactId + media_id: MediaId + generation_id: IndexGenerationId | None = None + job_id: JobId | None = None + kind: ArtifactKind + profile: str = Field(min_length=1) + mime_type: MimeType + byte_size: int = Field(gt=0) + sha256: Sha256 + state: ArtifactState + created_at: AwareDatetime + expires_at: AwareDatetime | None = None + + +class ActorOverlayProfile(StrEnum): + default = "default" + + +class CreateActorOverlayCommand(ApplicationModel): + cluster_id: ActorClusterId + profile: ActorOverlayProfile = ActorOverlayProfile.default + + +class SnippetProfile(StrEnum): + source = "source" + compatible_mp4 = "compatible_mp4" + + +class CreateSnippetCommand(ApplicationModel): + media_id: MediaId = Field( + description=( + "Source video ID, normally copied from a search or query result." + ) + ) + start_seconds: float = Field( + ge=0, + description="Inclusive clip start from the selected result, in seconds.", + ) + end_seconds: float = Field( + gt=0, + description="Exclusive clip end from the selected result, in seconds.", + ) + profile: SnippetProfile = Field( + default=SnippetProfile.compatible_mp4, + description=( + "Use compatible_mp4 for a broadly playable download; source " + "preserves source codecs in a Matroska container." + ), + ) + + def model_post_init(self, _context: Any) -> None: + if self.end_seconds <= self.start_seconds: + raise ValueError("end_seconds must be greater than start_seconds") + + +class IndexStatus(ApplicationModel): + schema_version: int = Field(ge=1) + state: str = Field(min_length=1) + stage: str = Field(min_length=1) + message: str = Field(min_length=1) + updated_at: AwareDatetime | None = None + summary: "IndexStatusSummary | None" = None + + +class IndexStatusSummary(ApplicationModel): + index_schema_version: int = Field(ge=1) + snapshot_id: IndexSnapshotId + media_count: int = Field( + ge=0, + description="Number of media items in the active index snapshot.", + ) + media_ids: tuple[MediaId, ...] = Field( + default=(), + max_length=INDEX_STATUS_MEDIA_ID_LIMIT, + description=( + "Stable IDs included in the active snapshot. The list is capped; " + "check media_ids_truncated before treating it as complete." + ), + ) + media_ids_truncated: bool = Field( + default=False, + description="Whether active snapshot media IDs were omitted by the cap.", + ) + modalities: tuple[str, ...] = () + + @model_validator(mode="after") + def _validate_media_id_window(self) -> "IndexStatusSummary": + if self.media_count < len(self.media_ids): + raise ValueError("media_count must cover every returned media ID") + if self.media_ids_truncated != ( + self.media_count > len(self.media_ids) + ): + raise ValueError( + "media_ids_truncated must reflect the returned media-ID window" + ) + return self + + +class FusionProfile(StrEnum): + reciprocal_rank = "rrf_v1" + + +class SearchCommand(ApplicationModel): + query: SearchQuery = Field(description="Text to match against indexed moments.") + modalities: tuple[Identifier, ...] = Field( + default=(), + description=( + "Indexed capabilities to search. Omit to use every searchable " + "capability in the active snapshot." + ), + ) + media_id: MediaId | None = Field( + default=None, + description=( + "Optional single-media filter. Provide a stable ID from list_media " + "to search only that video; omit it to rank results across every " + "media item in the active index snapshot." + ), + ) + top_k: int = Field( + default=10, + gt=0, + le=100, + description="Maximum fused moments to return across the selected scope.", + ) + + @field_validator("modalities") + @classmethod + def _unique_modalities( + cls, + values: tuple[Identifier, ...], + ) -> tuple[Identifier, ...]: + if len(values) != len(set(values)): + raise ValueError("Search modalities must be unique.") + return values + + +class SearchHit(ApplicationModel): + rank: int = Field(gt=0) + media_id: MediaId + video_id: VideoId + generation_id: IndexGenerationId + start: float = Field(ge=0) + end: float = Field(gt=0) + score: float + raw_distance: float + modality: str = Field(min_length=1) + source_id: str = Field(min_length=1) + metadata: dict[str, JsonValue] = Field(default_factory=dict) + + @field_validator("metadata") + @classmethod + def _reject_internal_metadata( + cls, + value: dict[str, JsonValue], + ) -> dict[str, JsonValue]: + forbidden: set[str] = set() + + def inspect(item: JsonValue) -> None: + if isinstance(item, dict): + for key, nested in item.items(): + if ( + key == "path" + or key == "storage_key" + or key.endswith("_path") + or key.endswith("_directory") + ): + forbidden.add(key) + inspect(nested) + elif isinstance(item, list): + for nested in item: + inspect(nested) + + inspect(value) + if forbidden: + raise ValueError( + "Search metadata contains internal location fields." + ) + return value + + @model_validator(mode="after") + def _validate_interval(self) -> "SearchHit": + if self.end <= self.start: + raise ValueError("end must be greater than start") + return self + + def to_dict(self) -> dict[str, Any]: + return self.model_dump(mode="json") + + +class SearchResult(ApplicationModel): + schema_version: int = INDEX_SCHEMA_VERSION + query_id: str = Field(min_length=1) + query: str = Field(min_length=1) + modality: str = Field(min_length=1) + hits: tuple[SearchHit, ...] = () + + def to_dict(self) -> dict[str, Any]: + return self.model_dump(mode="json") + + def to_prediction(self) -> dict[str, list[dict[str, Any]]]: + return { + self.query_id: [ + hit.model_dump(mode="json") for hit in self.hits + ] + } + + +class FusionProvenance(ApplicationModel): + profile: Literal[FusionProfile.reciprocal_rank] = ( + FusionProfile.reciprocal_rank + ) + rank_constant: int = Field(default=60, gt=0) + overlap_rule: Literal["connected_intervals"] = "connected_intervals" + requested_modalities: tuple[Identifier, ...] = () + searched_modalities: tuple[Identifier, ...] = () + + +class FusedMoment(ApplicationModel): + rank: int = Field(gt=0) + score: float = Field(gt=0) + media_id: MediaId + start: float = Field(ge=0) + end: float = Field(gt=0) + modalities: tuple[Identifier, ...] + hits: tuple[SearchHit, ...] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_fused_moment(self) -> "FusedMoment": + if self.end <= self.start: + raise ValueError("end must be greater than start") + if any(hit.media_id != self.media_id for hit in self.hits): + raise ValueError("Fused moment hits must belong to one media item.") + if set(self.modalities) != {hit.modality for hit in self.hits}: + raise ValueError("Fused moment modalities must match its hits.") + return self + + +class FusedSearchResult(ApplicationModel): + schema_version: int = INDEX_SCHEMA_VERSION + query_id: str = Field(min_length=1) + query: SearchQuery + modalities: tuple[Identifier, ...] + moments: tuple[FusedMoment, ...] = () + fusion: FusionProvenance + + +class QueryVideoCommand(ApplicationModel): + question: SearchQuery = Field( + description="Natural-language question grounded in indexed evidence." + ) + media_id: MediaId | None = Field( + default=None, + description=( + "Optional single-media filter. Provide a stable ID from list_media " + "to use evidence only from that video; omit it to use evidence " + "from every media item in the active index snapshot." + ), + ) + modalities: tuple[Identifier, ...] = Field( + default=(), + max_length=8, + description=( + "Indexed capabilities to use as evidence. Omit to use every " + "queryable capability in the active snapshot." + ), + ) + top_k: int = Field( + default=10, + gt=0, + le=50, + description="Maximum ranked evidence moments used for the answer.", + ) + + @field_validator("modalities") + @classmethod + def _unique_modalities( + cls, + values: tuple[Identifier, ...], + ) -> tuple[Identifier, ...]: + if len(values) != len(set(values)): + raise ValueError("Query modalities must be unique.") + return values + + +class SearchMomentsPlanStep(ApplicationModel): + kind: Literal["search_moments"] = "search_moments" + modality: Identifier + query: SearchQuery + + +class ActorOverviewPlanStep(ApplicationModel): + kind: Literal["actor_overview"] = "actor_overview" + + +QueryPlanStep = Annotated[ + SearchMomentsPlanStep | ActorOverviewPlanStep, + Field(discriminator="kind"), +] + + +class QueryPlan(ApplicationModel): + steps: tuple[QueryPlanStep, ...] = Field(min_length=1, max_length=8) + + +class QueryPlanningRequest(ApplicationModel): + question: SearchQuery + allowed_modalities: tuple[Identifier, ...] + actor_overview_allowed: bool = False + + +class QueryModelIdentity(ApplicationModel): + provider: Literal["ollama"] + model: str = Field(min_length=1, max_length=255) + + +class MomentEvidence(ApplicationModel): + kind: Literal["moment"] = "moment" + evidence_id: Sha256 + snapshot_id: IndexSnapshotId + media_id: MediaId + generation_id: IndexGenerationId + modality: Identifier + source_id: str = Field(min_length=1, max_length=512) + start: float = Field(ge=0) + end: float = Field(gt=0) + display_text: str | None = Field(default=None, max_length=4096) + hit: SearchHit + + @model_validator(mode="after") + def _validate_hit_identity(self) -> "MomentEvidence": + if ( + self.media_id != self.hit.media_id + or self.generation_id != self.hit.generation_id + or self.modality != self.hit.modality + or self.source_id != self.hit.source_id + or self.start != self.hit.start + or self.end != self.hit.end + ): + raise ValueError("Evidence identity must match its search hit.") + return self + + +class ActorEvidence(ApplicationModel): + kind: Literal["actor"] = "actor" + evidence_id: Sha256 + snapshot_id: IndexSnapshotId + media_id: MediaId + generation_id: IndexGenerationId + modality: Literal["actor"] = "actor" + cluster_id: ActorClusterId + start: float = Field(ge=0) + end: float = Field(ge=0) + detection_count: int = Field(ge=0) + display_text: str = Field(min_length=1, max_length=4096) + + +Evidence = Annotated[ + MomentEvidence | ActorEvidence, + Field(discriminator="kind"), +] + + +class DraftClaim(ApplicationModel): + text: str = Field(min_length=1, max_length=4096) + evidence_ids: tuple[Sha256, ...] = Field(min_length=1, max_length=10) + + @field_validator("evidence_ids") + @classmethod + def _unique_evidence_ids( + cls, + values: tuple[Sha256, ...], + ) -> tuple[Sha256, ...]: + if len(values) != len(set(values)): + raise ValueError("Draft evidence IDs must be unique.") + return values + + +class DraftAnswer(ApplicationModel): + claims: tuple[DraftClaim, ...] = Field(min_length=1, max_length=20) + + +class QuerySynthesisRequest(ApplicationModel): + question: SearchQuery + evidence: tuple[Evidence, ...] = Field(min_length=1, max_length=200) + + +class GroundedClaim(ApplicationModel): + text: str = Field(min_length=1, max_length=4096) + evidence_ids: tuple[Sha256, ...] = Field(min_length=1, max_length=10) + + @field_validator("evidence_ids") + @classmethod + def _unique_evidence_ids( + cls, + values: tuple[Sha256, ...], + ) -> tuple[Sha256, ...]: + if len(values) != len(set(values)): + raise ValueError("Claim evidence IDs must be unique.") + return values + + +class QueryAnswerMode(StrEnum): + generated = "generated" + evidence_only = "evidence_only" + no_evidence = "no_evidence" + + +class QueryAnswer(ApplicationModel): + schema_version: int = INDEX_SCHEMA_VERSION + question: SearchQuery + mode: QueryAnswerMode + plan: QueryPlan + model: QueryModelIdentity | None = None + claims: tuple[GroundedClaim, ...] = () + evidence: tuple[Evidence, ...] = Field(default=(), max_length=200) + moments: tuple[FusedMoment, ...] = () + fusion: FusionProvenance + fallback_reason: str | None = Field(default=None, max_length=512) + + @model_validator(mode="after") + def _validate_answer_grounding(self) -> "QueryAnswer": + evidence_ids = {item.evidence_id for item in self.evidence} + cited_ids = { + evidence_id + for claim in self.claims + for evidence_id in claim.evidence_ids + } + if not cited_ids.issubset(evidence_ids): + raise ValueError("Every claim citation must resolve to evidence.") + if self.mode == QueryAnswerMode.generated and not self.claims: + raise ValueError("Generated answers require grounded claims.") + if self.mode != QueryAnswerMode.generated and self.claims: + raise ValueError("Fallback answers must not contain generated claims.") + if self.mode == QueryAnswerMode.no_evidence and self.evidence: + raise ValueError("No-evidence answers cannot contain evidence.") + return self + + +class PrepareModelsCommand(ApplicationModel): + modalities: tuple[str, ...] + capability_options: Mapping[str, Mapping[str, JsonValue]] = Field( + default_factory=dict + ) + + +class DependencyCheckCommand(ApplicationModel): + modalities: tuple[str, ...] + include_runtime_checks: bool = True + include_models: bool = False + + +class DependencyCheckResult(ApplicationModel): + ok: bool + modalities: tuple[str, ...] + checks: tuple[CapabilityDependencyCheck, ...] + + +class PrepareModelsResult(ApplicationModel): + prepared: tuple[str, ...] + modalities: tuple[str, ...] + runtime: RuntimeProfile + + +JOB_SCHEMA_VERSION = 2 +JOB_PROGRESS_SCHEMA_VERSION = 1 + + +class IndexSnapshotReference(ApplicationModel): + snapshot_id: IndexSnapshotId + snapshot_sha256: Sha256 + + +class IndexJobRequest(ApplicationModel): + kind: Literal[JobKind.index] = JobKind.index + command: CreateIndexCommand + + +class MediaImportJobRequest(ApplicationModel): + kind: Literal[JobKind.media_import] = JobKind.media_import + upload_id: Identifier + + +class SearchJobRequest(ApplicationModel): + kind: Literal[JobKind.search] = JobKind.search + command: SearchCommand + snapshot: IndexSnapshotReference + + +class QueryJobRequest(ApplicationModel): + kind: Literal[JobKind.query] = JobKind.query + command: QueryVideoCommand + snapshot: IndexSnapshotReference + + +class SnippetJobRequest(ApplicationModel): + kind: Literal[JobKind.snippet] = JobKind.snippet + command: CreateSnippetCommand + + +class ActorOverlayJobRequest(ApplicationModel): + kind: Literal[JobKind.actor_overlay] = JobKind.actor_overlay + command: CreateActorOverlayCommand + snapshot: IndexSnapshotReference + + +class PrepareModelsJobRequest(ApplicationModel): + kind: Literal[JobKind.prepare_models] = JobKind.prepare_models + command: PrepareModelsCommand + + +JobRequest = Annotated[ + MediaImportJobRequest + | IndexJobRequest + | SearchJobRequest + | QueryJobRequest + | SnippetJobRequest + | ActorOverlayJobRequest + | PrepareModelsJobRequest, + Field(discriminator="kind"), +] + + +class IndexJobResult(ApplicationModel): + kind: Literal[JobKind.index] = JobKind.index + result: IndexResult + + +class MediaImportJobResult(ApplicationModel): + kind: Literal[JobKind.media_import] = JobKind.media_import + result: MediaAsset + + +class SearchJobResult(ApplicationModel): + kind: Literal[JobKind.search] = JobKind.search + result: FusedSearchResult + + +class QueryJobResult(ApplicationModel): + kind: Literal[JobKind.query] = JobKind.query + result: QueryAnswer + + +class ArtifactJobResult(ApplicationModel): + kind: Literal[JobKind.snippet, JobKind.actor_overlay] + result: Artifact + + +class PrepareModelsJobResult(ApplicationModel): + kind: Literal[JobKind.prepare_models] = JobKind.prepare_models + result: PrepareModelsResult + + +JobResult = Annotated[ + MediaImportJobResult + | IndexJobResult + | SearchJobResult + | QueryJobResult + | ArtifactJobResult + | PrepareModelsJobResult, + Field(discriminator="kind"), +] + + +class JobProgress(ApplicationModel): + schema_version: Literal[JOB_PROGRESS_SCHEMA_VERSION] = ( + JOB_PROGRESS_SCHEMA_VERSION + ) + stage: str = Field(min_length=1, max_length=128) + message: str = Field(min_length=1, max_length=512) + current: int | None = Field(default=None, ge=0) + total: int | None = Field(default=None, gt=0) + updated_at: AwareDatetime + + @model_validator(mode="after") + def _validate_position(self) -> "JobProgress": + if ( + self.current is not None + and self.total is not None + and self.current > self.total + ): + raise ValueError("current must not exceed total") + return self + + +class Job(ApplicationModel): + schema_version: Literal[JOB_SCHEMA_VERSION] = JOB_SCHEMA_VERSION + job_id: JobId + kind: JobKind + state: JobState + queue: JobQueue + progress: JobProgress | None = None + result: JobResult | None = None + error: ErrorDetail | None = None + recovery_attempts: int = Field(default=0, ge=0) + created_at: AwareDatetime | None = None + updated_at: AwareDatetime | None = None + + @model_validator(mode="after") + def _validate_terminal_payload(self) -> "Job": + if self.state == JobState.succeeded: + if self.result is None or self.error is not None: + raise ValueError( + "succeeded jobs require a result and no error" + ) + if self.result.kind != self.kind: + raise ValueError("job result kind must match job kind") + elif self.result is not None: + raise ValueError("only succeeded jobs may contain a result") + if self.state in {JobState.queued, JobState.running}: + if self.error is not None: + raise ValueError("active jobs may not contain an error") + elif self.state in {JobState.failed, JobState.recovery_exhausted}: + if self.error is None: + raise ValueError("failed jobs require a typed error") + return self + + +class ListJobsCommand(ApplicationModel): + page_size: int = Field(default=50, gt=0, le=100) + cursor: str | None = Field(default=None, min_length=1, max_length=512) + + +class JobPage(ApplicationModel): + items: tuple[Job, ...] = () + next_cursor: str | None = None diff --git a/src/vidxp/artifact_service.py b/src/vidxp/artifact_service.py new file mode 100644 index 0000000..b326a81 --- /dev/null +++ b/src/vidxp/artifact_service.py @@ -0,0 +1,378 @@ +from __future__ import annotations + +import hashlib +import json +from collections.abc import Callable +from pathlib import Path +from uuid import uuid4 + +from vidxp.application_models import ( + ActorOverlayProfile, + Artifact, + CreateSnippetCommand, + SnippetProfile, +) +from vidxp.core.artifacts import ( + ArtifactIntegrityError, + ArtifactKind, + ArtifactRecord, + ArtifactState, +) +from vidxp.core.media import InvalidMediaError, utc_now +from vidxp.execution import ExecutionContext, execution_context +from vidxp.media_service import MediaService +from vidxp.ports import ( + ActorRendererPort, + ArtifactCatalogPort, + ArtifactStorePort, + LocalFileResource, + MediaProbePort, + SnippetRendererPort, +) + + +class ArtifactUnavailableError(FileNotFoundError): + """Raised when an artifact is missing, expired, or fails integrity checks.""" + + +class ArtifactRequestError(ValueError): + """Raised when an artifact request is invalid for its source media.""" + + +class InvalidArtifactError(RuntimeError): + """Raised when rendered output is not valid media.""" + + +ARTIFACT_RENDER_CONTRACT_VERSION = 1 + + +def artifact_result(record: ArtifactRecord) -> Artifact: + return Artifact( + schema_version=record.schema_version, + artifact_id=record.artifact_id, + media_id=record.media_id, + generation_id=record.generation_id, + job_id=record.job_id, + kind=record.kind, + profile=record.profile, + mime_type=record.mime_type, + byte_size=record.byte_size, + sha256=record.sha256, + state=record.state, + created_at=record.created_at, + expires_at=record.expires_at, + ) + + +def _request_key(payload: dict) -> str: + encoded = json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + ).encode() + return hashlib.sha256(encoded).hexdigest() + + +class ArtifactQueryService: + """Read-only artifact boundary shared by worker and control-plane profiles.""" + + def __init__( + self, + *, + catalog: ArtifactCatalogPort, + store: ArtifactStorePort, + ) -> None: + self.catalog = catalog + self.store = store + + def get(self, artifact_id: str) -> Artifact: + return artifact_result(self.require_record(artifact_id)) + + def require_record(self, artifact_id: str) -> ArtifactRecord: + record = self.catalog.get_artifact(artifact_id) + if record is None: + raise ArtifactUnavailableError("The artifact is unavailable.") + self._require_ready(record) + return record + + @staticmethod + def _require_ready(record: ArtifactRecord) -> None: + if ( + record.state != ArtifactState.ready + or ( + record.expires_at is not None + and record.expires_at <= utc_now() + ) + ): + raise ArtifactUnavailableError("The artifact is unavailable.") + + def content(self, artifact_id: str) -> LocalFileResource: + record = self.require_record(artifact_id) + path = self._verified_content(record) + suffix = ".mp4" if record.mime_type == "video/mp4" else ".mkv" + return LocalFileResource( + path=path, + filename=f"{record.kind.value}-{record.artifact_id}{suffix}", + mime_type=record.mime_type, + byte_size=record.byte_size, + etag=record.sha256, + ) + + def _verified_content(self, record: ArtifactRecord) -> Path: + try: + return self.store.verify( + record.storage_key, + sha256=record.sha256, + byte_size=record.byte_size, + ) + except (FileNotFoundError, PermissionError) as exc: + raise ArtifactUnavailableError( + "The artifact content is unavailable." + ) from exc + except RuntimeError as exc: + raise ArtifactIntegrityError( + "The artifact failed its integrity check." + ) from exc + + +class ArtifactService(ArtifactQueryService): + def __init__( + self, + *, + catalog: ArtifactCatalogPort, + store: ArtifactStorePort, + media: MediaService, + probe: MediaProbePort, + actor_renderer: ActorRendererPort, + snippet_renderer: SnippetRendererPort, + max_snippet_duration_seconds: float, + ) -> None: + super().__init__(catalog=catalog, store=store) + self.media = media + self.probe = probe + self.actor_renderer = actor_renderer + self.snippet_renderer = snippet_renderer + self.max_snippet_duration_seconds = max_snippet_duration_seconds + + def create_actor_overlay( + self, + *, + media_id: str, + generation_id: str, + cluster_id: str, + detections: list[dict], + profile: ActorOverlayProfile, + job_id: str | None = None, + execution: ExecutionContext | None = None, + ) -> Artifact: + active_execution = execution_context(execution) + active_execution.checkpoint() + source = self.media.content(media_id) + request_key = _request_key( + { + "kind": ArtifactKind.actor_overlay, + "render_contract_version": ARTIFACT_RENDER_CONTRACT_VERSION, + "media_sha256": source.etag, + "generation_id": generation_id, + "cluster_id": cluster_id, + "profile": profile, + } + ) + return self._create( + media_id=media_id, + generation_id=generation_id, + kind=ArtifactKind.actor_overlay, + profile=profile.value, + request_key=request_key, + suffix=".mp4", + expected_mime_type="video/mp4", + job_id=job_id, + execution=active_execution, + render=lambda destination: self.actor_renderer.render( + source.path, + destination, + cluster_id, + detections, + cancellation=active_execution.cancellation, + progress=active_execution.progress, + ), + ) + + def create_snippet( + self, + command: CreateSnippetCommand, + *, + job_id: str | None = None, + execution: ExecutionContext | None = None, + ) -> Artifact: + active_execution = execution_context(execution) + active_execution.checkpoint() + media_record = self.media.require_record(command.media_id) + duration = command.end_seconds - command.start_seconds + if command.end_seconds > media_record.duration_seconds: + raise ArtifactRequestError( + "The snippet interval exceeds the media duration." + ) + if duration > self.max_snippet_duration_seconds: + raise ArtifactRequestError( + "The snippet exceeds the configured duration limit." + ) + source = self.media.content(command.media_id) + compatible = command.profile == SnippetProfile.compatible_mp4 + request_key = _request_key( + { + "kind": ArtifactKind.snippet, + "render_contract_version": ARTIFACT_RENDER_CONTRACT_VERSION, + "media_sha256": source.etag, + "start_seconds": command.start_seconds, + "end_seconds": command.end_seconds, + "profile": command.profile, + } + ) + return self._create( + media_id=command.media_id, + generation_id=None, + kind=ArtifactKind.snippet, + profile=command.profile.value, + request_key=request_key, + suffix=".mp4" if compatible else ".mkv", + expected_mime_type=( + "video/mp4" if compatible else "video/x-matroska" + ), + job_id=job_id, + execution=active_execution, + render=lambda destination: self.snippet_renderer.render( + source.path, + destination, + start_seconds=command.start_seconds, + end_seconds=command.end_seconds, + compatible_mp4=compatible, + cancellation=active_execution.cancellation, + progress=active_execution.progress, + ), + ) + + def _create( + self, + *, + media_id: str, + generation_id: str | None, + kind: ArtifactKind, + profile: str, + request_key: str, + suffix: str, + expected_mime_type: str, + job_id: str | None, + execution: ExecutionContext, + render: Callable[[Path], None], + ) -> Artifact: + execution.checkpoint() + cached = self.catalog.get_artifact_by_request(request_key) + if cached is not None: + try: + self._require_ready(cached) + self._verified_content(cached) + except (ArtifactIntegrityError, ArtifactUnavailableError): + self.catalog.invalidate_artifact_request( + request_key, + cached.artifact_id, + ) + else: + execution.report( + { + "stage": "complete", + "message": "Reused the existing ready artifact.", + "current": 1, + "total": 1, + } + ) + return artifact_result(cached) + + artifact_id = execution.operation_id or uuid4().hex + staged = self.store.stage(artifact_id, suffix=suffix) + stored = self.store.recover(artifact_id, suffix=suffix) + try: + if stored is None: + execution.report( + { + "stage": "rendering", + "message": "Rendering the requested video artifact.", + } + ) + render(staged.path) + execution.checkpoint() + artifact_path = staged.path + else: + execution.report( + { + "stage": "recovering", + "message": "Recovering the published video artifact.", + } + ) + artifact_path = stored.local_path + execution.report( + { + "stage": "validating", + "message": "Validating the rendered video artifact.", + } + ) + try: + probed = self.probe.probe(artifact_path) + except InvalidMediaError as exc: + raise InvalidArtifactError( + "The rendered artifact is not valid video." + ) from exc + if probed.detected_mime_type != expected_mime_type: + raise InvalidArtifactError( + "The rendered artifact does not match its output profile." + ) + execution.checkpoint() + if stored is None: + execution.report( + { + "stage": "publishing", + "message": "Publishing the validated video artifact.", + } + ) + stored = self.store.publish(staged) + execution.checkpoint() + record = ArtifactRecord( + artifact_id=artifact_id, + media_id=media_id, + generation_id=generation_id, + request_key=request_key, + kind=kind, + profile=profile, + mime_type=probed.detected_mime_type, + byte_size=stored.byte_size, + sha256=stored.sha256, + storage_key=stored.storage_key, + job_id=job_id, + state=ArtifactState.ready, + created_at=utc_now(), + ) + authoritative = self.catalog.put_artifact(record) + if authoritative.artifact_id != artifact_id: + self._delete_quietly(stored.storage_key) + stored = None + except BaseException: + if stored is not None: + self._delete_quietly(stored.storage_key) + raise + finally: + self.store.discard(staged) + execution.report( + { + "stage": "complete", + "message": "The video artifact is ready.", + "current": 1, + "total": 1, + } + ) + return artifact_result(authoritative) + + def _delete_quietly(self, storage_key: str) -> None: + try: + self.store.delete(storage_key) + except OSError: + pass diff --git a/src/vidxp/authentication.py b/src/vidxp/authentication.py new file mode 100644 index 0000000..8814914 --- /dev/null +++ b/src/vidxp/authentication.py @@ -0,0 +1,262 @@ +from __future__ import annotations + +from dataclasses import dataclass +from hmac import compare_digest +from typing import Any, Protocol + +from vidxp.application_models import ( + ApplicationError, + ComponentReadiness, + ErrorCategory, + Principal, +) +from vidxp.settings import HttpAuthMode, VidXPSettings + + +class Authenticator(Protocol): + def authenticate(self, token: str | None) -> Principal: ... + + def readiness(self) -> ComponentReadiness: ... + + +@dataclass(frozen=True) +class AuthenticatedBearer: + principal: Principal + expires_at: int | None + resource: str | None + claims: dict[str, Any] + + +def _ready() -> ComponentReadiness: + return ComponentReadiness( + name="authentication", + ready=True, + message="The HTTP authentication profile is ready.", + ) + + +def _authentication_error() -> ApplicationError: + return ApplicationError( + "authentication_required", + ErrorCategory.authentication, + "Valid bearer authentication is required.", + ) + + +def _authorization_error() -> ApplicationError: + return ApplicationError( + "insufficient_scope", + ErrorCategory.authorization, + "The authenticated principal lacks a required scope.", + ) + + +class LocalAuthenticator: + def authenticate(self, token: str | None) -> Principal: + del token + return Principal(subject="local", scopes=frozenset({"*"})) + + def readiness(self) -> ComponentReadiness: + return _ready() + + +class StaticBearerAuthenticator: + def __init__(self, expected_token: str) -> None: + self._expected_token = expected_token + + def authenticate(self, token: str | None) -> Principal: + if token is None or not compare_digest(token, self._expected_token): + raise _authentication_error() + return Principal( + subject="static", + client_id="static", + scopes=frozenset({"*"}), + ) + + def readiness(self) -> ComponentReadiness: + return _ready() + + +class OIDCBearerAuthenticator: + def __init__( + self, + *, + issuer: str, + audience: str, + jwks_url: str, + algorithms: tuple[str, ...], + required_scopes: tuple[str, ...], + jwks_client: Any | None = None, + ) -> None: + import jwt + + self._jwt = jwt + self._issuer = issuer + self._audience = audience + self._algorithms = algorithms + self._required_scopes = frozenset(required_scopes) + self._jwks = ( + jwks_client + if jwks_client is not None + else jwt.PyJWKClient( + jwks_url, + cache_keys=True, + cache_jwk_set=True, + lifespan=300, + timeout=5, + ) + ) + self._jwks_url = jwks_url + + def for_audience( + self, + audience: str, + *, + required_scopes: tuple[str, ...] = (), + ) -> "OIDCBearerAuthenticator": + return OIDCBearerAuthenticator( + issuer=self._issuer, + audience=audience, + jwks_url=self._jwks_url, + algorithms=self._algorithms, + required_scopes=required_scopes, + jwks_client=self._jwks, + ) + + def authenticate(self, token: str | None) -> Principal: + return self.authenticate_bearer(token).principal + + def authenticate_bearer( + self, + token: str | None, + ) -> AuthenticatedBearer: + if token is None: + raise _authentication_error() + try: + signing_key = self._jwks.get_signing_key_from_jwt(token) + claims = self._jwt.decode( + token, + signing_key, + algorithms=list(self._algorithms), + audience=self._audience, + issuer=self._issuer, + options={ + "require": ["exp", "iss", "aud", "sub"], + "verify_signature": True, + "verify_exp": True, + "verify_nbf": True, + "verify_iss": True, + "verify_aud": True, + }, + ) + except self._jwt.PyJWTError as exc: + raise _authentication_error() from exc + subject = claims.get("sub") + if not isinstance(subject, str) or not subject: + raise _authentication_error() + scopes = _token_scopes(claims) + if not self._required_scopes.issubset(scopes): + raise _authorization_error() + client_id = claims.get("client_id") + principal = Principal( + subject=subject, + client_id=client_id if isinstance(client_id, str) else None, + scopes=scopes, + ) + expires_at = claims.get("exp") + resource = claims.get("aud") + return AuthenticatedBearer( + principal=principal, + expires_at=( + expires_at if isinstance(expires_at, int) else None + ), + resource=( + resource + if isinstance(resource, str) + else self._audience + ), + claims=dict(claims), + ) + + def readiness(self) -> ComponentReadiness: + try: + key_set = self._jwks.get_jwk_set() + usable = any( + self._usable_signing_key(key) + for key in key_set.keys + ) + if not usable: + raise ValueError( + "The OIDC JWKS contains no usable signing keys." + ) + except Exception: + return ComponentReadiness( + name="authentication", + ready=False, + message="The OIDC signing-key service is unavailable.", + ) + return _ready() + + def _usable_signing_key(self, key) -> bool: + if ( + key.public_key_use not in {None, "sig"} + or key.algorithm_name not in self._algorithms + ): + return False + data = getattr(key, "_jwk_data", {}) + key_operations = ( + data.get("key_ops") if isinstance(data, dict) else None + ) + return ( + key_operations is None + or ( + isinstance(key_operations, list) + and "verify" in key_operations + ) + ) + + +def _token_scopes(claims: dict) -> frozenset[str]: + scope = claims.get("scope") + if isinstance(scope, str): + values = scope.split() + else: + scp = claims.get("scp") + if isinstance(scp, str): + values = scp.split() + elif isinstance(scp, list) and all( + isinstance(value, str) for value in scp + ): + values = scp + else: + values = [] + return frozenset(value for value in values if value) + + +def create_authenticator( + settings: VidXPSettings, + *, + audience: str | None = None, + required_scopes: tuple[str, ...] | None = None, +) -> Authenticator: + if settings.http_auth_mode == HttpAuthMode.none: + return LocalAuthenticator() + if settings.http_auth_mode == HttpAuthMode.static: + assert settings.http_static_bearer_token is not None + return StaticBearerAuthenticator( + settings.http_static_bearer_token.get_secret_value() + ) + assert settings.http_oidc_issuer is not None + assert settings.http_oidc_audience is not None + assert settings.http_oidc_jwks_url is not None + return OIDCBearerAuthenticator( + issuer=settings.http_oidc_issuer, + audience=audience or settings.http_oidc_audience, + jwks_url=settings.http_oidc_jwks_url, + algorithms=settings.http_oidc_algorithms, + required_scopes=( + settings.http_required_scopes + if required_scopes is None + else required_scopes + ), + ) diff --git a/src/vidxp/authorization.py b/src/vidxp/authorization.py new file mode 100644 index 0000000..f597c26 --- /dev/null +++ b/src/vidxp/authorization.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from enum import StrEnum + +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + Principal, +) + + +class RepositoryPermission(StrEnum): + read = "vidxp.read" + write = "vidxp.write" + admin = "vidxp.admin" + + +class AuthorizationPolicy: + """Repository-wide authorization shared by HTTP and future MCP adapters.""" + + def require( + self, + principal: Principal, + permission: RepositoryPermission, + ) -> Principal: + scopes = principal.scopes + if ( + "*" in scopes + or RepositoryPermission.admin.value in scopes + or permission.value in scopes + ): + return principal + raise ApplicationError( + "insufficient_scope", + ErrorCategory.authorization, + "The authenticated principal lacks the required repository scope.", + details={"required_scope": permission.value}, + ) diff --git a/src/vidxp/benchmarks/cli.py b/src/vidxp/benchmarks/cli.py index fd84ca0..34fa5e2 100644 --- a/src/vidxp/benchmarks/cli.py +++ b/src/vidxp/benchmarks/cli.py @@ -6,21 +6,193 @@ import typer from rich import print as rich_print +from rich.console import Console +from rich.progress import ( + BarColumn, + DownloadColumn, + Progress, + TextColumn, + TimeRemainingColumn, + TransferSpeedColumn, +) from vidxp.benchmarks.didemo import run_didemo from vidxp.benchmarks.hirest import ( HIREST_DEFAULT_WINDOW_FRACTION, run_hirest, ) +from vidxp.benchmarks.prepare import ( + PreparationPlan, + execute_preparation, + plan_didemo, + plan_hirest, +) +from vidxp.app_paths import available_storage_bytes +from vidxp.capabilities.registry import create_capability_registry from vidxp.cli_support import ( OutputFormat, effective_output_format, emit_json, + emit_progress, state_from_context, ) +from vidxp.dependencies import ( + active_requirements, + inspect_requirement, + packaged_requirements, +) -app = typer.Typer(help="Run official benchmark adapters.") +app = typer.Typer(help="Prepare and run official benchmark adapters.") +prepare_app = typer.Typer( + help="Download, verify, and arrange pinned benchmark inputs." +) +app.add_typer(prepare_app, name="prepare") + + +def _format_bytes(value: int) -> str: + size = float(value) + for unit in ("bytes", "KiB", "MiB", "GiB", "TiB"): + if size < 1024 or unit == "TiB": + precision = 0 if unit == "bytes" else 1 + return f"{size:.{precision}f} {unit}" + size /= 1024 + raise AssertionError("unreachable") + + +def _show_preparation_plan(plan: PreparationPlan) -> None: + typer.secho("Benchmark preparation plan", bold=True) + typer.echo(f" Benchmark: {plan.benchmark} {plan.split}") + typer.echo( + f" Selection: {plan.selected_count} item(s), " + f"{len(plan.selected_video_names)} video(s)" + ) + typer.echo(f" Destination: {plan.root}") + typer.echo( + f" New files: {plan.download_count}; " + "maximum additional storage: " + f"{_format_bytes(plan.additional_bytes)}" + ) + free_bytes = available_storage_bytes(plan.root) + if free_bytes is not None: + typer.echo(f" Free space at destination: {_format_bytes(free_bytes)}") + if free_bytes < plan.additional_bytes: + typer.secho( + " Warning: the destination may not have enough free space. " + "Choose another --output-directory or free space first.", + fg=typer.colors.YELLOW, + ) + replacements = [ + resource + for resource in plan.resources + if resource.replacement_for is not None + ] + for resource in replacements: + typer.secho( + " Documented replacement: " + f"{resource.replacement_for} -> {resource.url}", + fg=typer.colors.YELLOW, + ) + + +def _execute_preparation_plan( + plan: PreparationPlan, + *, + state, + yes: bool, + json_output: bool, +) -> None: + output_format = effective_output_format(state, json_output) + if output_format == OutputFormat.rich: + _show_preparation_plan(plan) + if plan.download_count and not yes: + if output_format == OutputFormat.json: + raise typer.BadParameter( + "Benchmark downloads require --yes with JSON output.", + param_hint="--yes", + ) + typer.confirm( + "Download and prepare these benchmark inputs?", + abort=True, + ) + network_bytes = plan.network_bytes + if ( + state.quiet + or output_format == OutputFormat.json + or network_bytes == 0 + ): + result = execute_preparation( + plan, + ffprobe=state.settings.ffprobe_executable, + ffmpeg=state.settings.ffmpeg_executable, + ) + else: + console = Console(stderr=True) + with Progress( + TextColumn("{task.description}"), + BarColumn(), + DownloadColumn(), + TransferSpeedColumn(), + TimeRemainingColumn(), + console=console, + ) as progress: + task = progress.add_task( + "Downloading benchmark inputs", + total=network_bytes or None, + ) + + def advance(name: str, amount: int) -> None: + progress.update( + task, + description=f"Downloading {name}", + advance=amount, + ) + + result = execute_preparation( + plan, + ffprobe=state.settings.ffprobe_executable, + ffmpeg=state.settings.ffmpeg_executable, + progress=advance, + ) + if output_format == OutputFormat.json: + emit_json(result) + return + typer.secho("Benchmark inputs are ready.", fg=typer.colors.GREEN) + typer.echo(f"Manifest: {plan.manifest_path}") + typer.echo("Run:") + typer.secho(plan.command, bold=True) + + +def _require_benchmark_dependencies( + capability: str, + *, + include_benchmark_extra: bool = False, +) -> None: + requirements = list( + create_capability_registry().requirements_for((capability,)) + ) + extras = [capability] + if include_benchmark_extra: + requirements.extend( + active_requirements(packaged_requirements("vidxp.benchmarks")) + ) + extras.append("benchmarks") + failures = [ + check + for requirement in dict.fromkeys(requirements) + if not (check := inspect_requirement(requirement))["ok"] + ] + if not failures: + return + unavailable = ", ".join( + f"{failure['requirement']} ({failure['error']})" + for failure in failures + ) + extra = ",".join(extras) + raise typer.BadParameter( + f"Benchmark dependencies are unavailable: {unavailable}. " + f'Install them with: pip install "vidxp[{extra}]"' + ) def _annotation_indices(value: str | None) -> list[int] | None: @@ -46,7 +218,13 @@ def _annotation_indices(value: str | None) -> list[int] | None: def _pair_file(path: Path | None) -> list[tuple[str, str]] | None: if path is None: return None - payload = json.loads(path.read_text(encoding="utf-8")) + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise typer.BadParameter( + "The HiREST pair file must contain valid readable JSON.", + param_hint="--pairs", + ) from exc if not isinstance(payload, list) or not payload: raise typer.BadParameter( "The HiREST pair file must be a non-empty JSON list." @@ -66,6 +244,184 @@ def _pair_file(path: Path | None) -> list[tuple[str, str]] | None: return pairs +def _media_override_file(path: Path | None) -> dict[str, Path] | None: + if path is None: + return None + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise typer.BadParameter( + "The DiDeMo media override file must contain valid readable JSON.", + param_hint="--media-overrides", + ) from exc + if not isinstance(payload, dict) or not payload: + raise typer.BadParameter( + "The DiDeMo media override file must be a non-empty JSON object.", + param_hint="--media-overrides", + ) + overrides: dict[str, Path] = {} + for video_name, replacement in payload.items(): + if ( + not isinstance(video_name, str) + or not video_name.strip() + or not isinstance(replacement, str) + or not replacement.strip() + ): + raise typer.BadParameter( + "Each DiDeMo media override must map a video name to a path.", + param_hint="--media-overrides", + ) + candidate = Path(replacement) + if not candidate.is_absolute(): + candidate = path.parent / candidate + candidate = candidate.resolve() + if not candidate.is_file(): + raise typer.BadParameter( + f"DiDeMo replacement media was not found: {candidate}", + param_hint="--media-overrides", + ) + overrides[video_name] = candidate + return overrides + + +@prepare_app.command("didemo") +def prepare_didemo_command( + ctx: typer.Context, + split: Annotated[ + Literal["validation", "test"], + typer.Option(help="Official split to prepare."), + ] = "test", + annotation_indices: Annotated[ + str | None, + typer.Option( + help=( + "Comma-separated zero-based annotation indices. " + "Omit to prepare the full selected split." + ) + ), + ] = None, + output_directory: Annotated[ + Path | None, + typer.Option( + file_okay=False, + help=( + "Preparation destination. Defaults to the VidXP application " + "data directory." + ), + ), + ] = None, + yes: Annotated[ + bool, + typer.Option( + "--yes", + "-y", + help="Confirm the displayed download and replacement plan.", + ), + ] = False, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Prepare verified DiDeMo artifacts and selected official videos.""" + + state = state_from_context(ctx) + root = ( + output_directory + if output_directory is not None + else state.settings.data_dir / "benchmarks" / "didemo" + ) + if ( + not state.quiet + and effective_output_format(state, json_output) == OutputFormat.rich + ): + emit_progress( + "Inspecting pinned DiDeMo metadata and download sizes..." + ) + plan = plan_didemo( + root=root, + split=split, + annotation_indices=_annotation_indices(annotation_indices), + ffprobe=state.settings.ffprobe_executable, + ffmpeg=state.settings.ffmpeg_executable, + ) + _execute_preparation_plan( + plan, + state=state, + yes=yes, + json_output=json_output, + ) + + +@prepare_app.command("hirest") +def prepare_hirest_command( + ctx: typer.Context, + split: Annotated[ + Literal["validation", "test"], + typer.Option(help="Official split to prepare."), + ] = "validation", + pairs: Annotated[ + Path | None, + typer.Option( + exists=True, + dir_okay=False, + help=( + "Optional JSON list of {prompt, video} pairs for a " + "declared smoke subset." + ), + ), + ] = None, + output_directory: Annotated[ + Path | None, + typer.Option( + file_okay=False, + help=( + "Preparation destination. Defaults to the VidXP application " + "data directory." + ), + ), + ] = None, + yes: Annotated[ + bool, + typer.Option( + "--yes", + "-y", + help="Confirm the displayed download plan.", + ), + ] = False, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Prepare verified HiREST artifacts and released transcripts.""" + + state = state_from_context(ctx) + root = ( + output_directory + if output_directory is not None + else state.settings.data_dir / "benchmarks" / "hirest" + ) + if ( + not state.quiet + and effective_output_format(state, json_output) == OutputFormat.rich + ): + emit_progress( + "Inspecting pinned HiREST metadata and download sizes..." + ) + plan = plan_hirest( + root=root, + split=split, + pairs=_pair_file(pairs), + ) + _execute_preparation_plan( + plan, + state=state, + yes=yes, + json_output=json_output, + ) + + @app.command("didemo") def didemo_command( ctx: typer.Context, @@ -90,13 +446,31 @@ def didemo_command( ) ), ] = None, + media_overrides: Annotated[ + Path | None, + typer.Option( + exists=True, + dir_okay=False, + help=( + "JSON object mapping an official video name to documented " + "replacement media. Relative paths resolve beside the file." + ), + ), + ] = None, chunk_pooling: Annotated[ Literal["max", "mean"], typer.Option( help="Pool sampled frame scores within each five-second chunk." ), ] = "max", - frame_stride: Annotated[int, typer.Option(min=1)] = 1, + scene_sample_fps: Annotated[ + float, + typer.Option( + "--scene-sample-fps", + min=0.01, + help="Target time-based scene samples per second.", + ), + ] = 1.0, reset: Annotated[bool, typer.Option()] = False, json_output: Annotated[ bool, @@ -105,6 +479,7 @@ def didemo_command( ) -> None: """Run DiDeMo scene retrieval and its official evaluator.""" + _require_benchmark_dependencies("scene") state = state_from_context(ctx) metrics = run_didemo( annotations_path=annotations, @@ -113,11 +488,12 @@ def didemo_command( run_id=run_id, output_root=output_root, annotation_indices=_annotation_indices(annotation_indices), - frame_stride=frame_stride, + media_overrides=_media_override_file(media_overrides), + scene_sample_fps=scene_sample_fps, split=split, chunk_pooling=chunk_pooling, reset=reset, - device=state.service.device or "cpu", + device=state.settings.runtime_backend, ) if effective_output_format(state, json_output) == OutputFormat.json: emit_json(metrics) @@ -169,6 +545,16 @@ def hirest_command( ) -> None: """Run HiREST released-ASR retrieval; score validation predictions.""" + if not 0 < temporal_window_fraction < 1: + raise typer.BadParameter( + "The temporal window fraction must be greater than zero and " + "less than one.", + param_hint="--temporal-window-fraction", + ) + _require_benchmark_dependencies( + "dialogue", + include_benchmark_extra=True, + ) state = state_from_context(ctx) metrics = run_hirest( ground_truth_path=ground_truth, @@ -182,7 +568,7 @@ def hirest_command( split=split, temporal_window_fraction=temporal_window_fraction, reset=reset, - device=state.service.device or "cpu", + device=state.settings.runtime_backend, ) if effective_output_format(state, json_output) == OutputFormat.json: emit_json(metrics) diff --git a/src/vidxp/benchmarks/common.py b/src/vidxp/benchmarks/common.py index 5df276a..f9d2853 100644 --- a/src/vidxp/benchmarks/common.py +++ b/src/vidxp/benchmarks/common.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json import os import subprocess @@ -9,11 +10,36 @@ from vidxp.core.manifest import sha256_file, utc_now, write_json_atomic +def _benchmark_uuid4(*parts: str) -> str: + digest = bytearray( + hashlib.sha256("\0".join(parts).encode("utf-8")).digest()[:16] + ) + digest[6] = (digest[6] & 0x0F) | 0x40 + digest[8] = (digest[8] & 0x3F) | 0x80 + return digest.hex() + + +def benchmark_generation_id( + benchmark: str, + split: str, + run_id: str, +) -> str: + """Return a stable UUID4-shaped identity for a resumable benchmark run.""" + + return _benchmark_uuid4("generation", benchmark, split, run_id) + + +def benchmark_media_id(benchmark: str, official_video_id: str) -> str: + """Map an official dataset video key to VidXP's opaque media ID shape.""" + + return _benchmark_uuid4("media", benchmark, official_video_id) + + def verify_artifact( path: str | Path, *, name: str, - expected_sha256: str, + expected_sha256: str | Sequence[str], source: str, revision: str, ) -> dict[str, Any]: @@ -21,9 +47,15 @@ def verify_artifact( if not artifact.is_file(): raise FileNotFoundError(f"{name} not found: {artifact}") actual_sha256 = sha256_file(artifact) - if actual_sha256 != expected_sha256: + accepted = ( + (expected_sha256,) + if isinstance(expected_sha256, str) + else tuple(expected_sha256) + ) + if actual_sha256 not in accepted: raise ValueError( - f"{name} checksum mismatch: expected {expected_sha256}, " + f"{name} checksum mismatch: expected one of " + f"{', '.join(accepted)}, " f"received {actual_sha256}." ) return { diff --git a/src/vidxp/benchmarks/didemo.py b/src/vidxp/benchmarks/didemo.py index bdf6752..11f6d89 100644 --- a/src/vidxp/benchmarks/didemo.py +++ b/src/vidxp/benchmarks/didemo.py @@ -11,6 +11,8 @@ from vidxp.benchmarks.common import ( append_failure, + benchmark_generation_id, + benchmark_media_id, ensure_adapter_outputs, record_adapter_manifest, run_logged_evaluator, @@ -18,9 +20,14 @@ ) from vidxp.capabilities.scene.operations import search_scene from vidxp.capabilities.schemas import SearchHit +from vidxp.capabilities.registry import create_capability_registry from vidxp.core.contracts import IndexConfig, VideoSource -from vidxp.core.manifest import write_json_atomic +from vidxp.core.manifest import ManifestStore, write_json_atomic from vidxp.core.runner import run_index +from vidxp.core.storage import IndexStorage +from vidxp.infrastructure.local_index import LOCAL_INDEX_RUNTIME_CHECKS +from vidxp.runtime import ModelRuntime +from vidxp.settings import VidXPSettings DIDEMO_REVISION = "b6a555c8134581305d0ed4716fbc192860e0b88c" @@ -32,6 +39,9 @@ "b0364cc256553332feb19d46bcc4cd2b09774949fe6c0b25e7ed0ff3c6aefebb" ) DIDEMO_EVALUATOR_SHA256 = ( + "9ec3e7a171272eb3551b0eaa7bbe9292131ad5cf34fd5c1e02c0fc4a11234df6" +) +DIDEMO_EVALUATOR_CRLF_SHA256 = ( "4754bb320564e5d2e7c633e0b660e87feca7f00fa73269e50140e81ffb4ca762" ) DIDEMO_MOMENTS = ( @@ -40,8 +50,7 @@ ) -def load_annotations(path: str | Path) -> list[dict[str, Any]]: - payload = json.loads(Path(path).read_text(encoding="utf-8")) +def parse_annotations(payload: Any) -> list[dict[str, Any]]: if not isinstance(payload, list) or not payload: raise ValueError("DiDeMo annotations must be a non-empty JSON list.") required = { @@ -72,7 +81,13 @@ def load_annotations(path: str | Path) -> list[dict[str, Any]]: raise ValueError( f"DiDeMo annotation {index} exceeds num_segments." ) - return payload + return [dict(annotation) for annotation in payload] + + +def load_annotations(path: str | Path) -> list[dict[str, Any]]: + return parse_annotations( + json.loads(Path(path).read_text(encoding="utf-8")) + ) def select_annotations( @@ -235,7 +250,10 @@ def _verified_artifacts( verify_artifact( evaluator_path, name="DiDeMo evaluator", - expected_sha256=DIDEMO_EVALUATOR_SHA256, + expected_sha256=( + DIDEMO_EVALUATOR_SHA256, + DIDEMO_EVALUATOR_CRLF_SHA256, + ), source=( f"{DIDEMO_REPOSITORY}/blob/{DIDEMO_REVISION}" "/utils/eval.py" @@ -253,7 +271,7 @@ def _video_sources( ) -> list[VideoSource]: return [ VideoSource( - video_id=video_name, + video_id=benchmark_media_id("didemo", video_name), path=resolve_media( media_directory, video_name, @@ -271,6 +289,8 @@ def _generate_predictions( config: IndexConfig, manifest: Mapping[str, Any], chunk_pooling: Literal["max", "mean"], + runtime: ModelRuntime, + storage: IndexStorage, ) -> list[list[list[int]]]: scene_counts = { video_id: int(video["summary"]["scene_frames"]) @@ -278,13 +298,18 @@ def _generate_predictions( } predictions = [] for annotation in annotations: - video_id = str(annotation["video"]) + video_id = benchmark_media_id( + "didemo", + str(annotation["video"]), + ) result = search_scene( str(annotation["description"]), config=config, top_k=scene_counts[video_id], video_id=video_id, query_id=str(annotation["annotation_id"]), + runtime=runtime, + storage=storage, ) predictions.append( [ @@ -354,7 +379,7 @@ def run_didemo( output_root: str | Path = "benchmark_runs", annotation_indices: Sequence[int] | None = None, media_overrides: Mapping[str, str | Path] | None = None, - frame_stride: int = 1, + scene_sample_fps: float = 1.0, device: str = "cpu", split: Literal["validation", "test"] = "test", chunk_pooling: Literal["max", "mean"] = "max", @@ -378,11 +403,24 @@ def run_didemo( split=split, run_id=run_id, enabled_modalities=("scene",), - frame_stride=frame_stride, + capability_options={ + "scene": {"sample_fps": scene_sample_fps}, + }, device=device, output_root=output_root, + generation_id=benchmark_generation_id("didemo", split, run_id), ) run_directory = config.run_directory + registry = create_capability_registry( + platform_runtime_checks=LOCAL_INDEX_RUNTIME_CHECKS + ) + runtime = ModelRuntime( + VidXPSettings( + repository_root=run_directory, + runtime_backend=device, + ), + allowed_specs=registry.model_specs(), + ) ensure_adapter_outputs(run_directory) subset = { "label": ( @@ -397,29 +435,40 @@ def run_didemo( "video_count": len({item["video"] for item in annotations}), } try: - manifest = run_index( - _video_sources( + with IndexStorage(config) as storage: + manifest = run_index( + _video_sources( + annotations, + media_directory=media_directory, + media_overrides=media_overrides, + ), + config, + reset=reset, + storage=storage, + manifest_store=ManifestStore( + config, + registry=registry, + runtime=runtime, + ), + registry=registry, + runtime=runtime, + ) + ensure_adapter_outputs(run_directory) + record_adapter_manifest( + run_directory, + benchmark="didemo", + subset=subset, + artifacts=artifacts, + state="predicting", + ) + predictions = _generate_predictions( annotations, - media_directory=media_directory, - media_overrides=media_overrides, - ), - config, - reset=reset, - ) - ensure_adapter_outputs(run_directory) - record_adapter_manifest( - run_directory, - benchmark="didemo", - subset=subset, - artifacts=artifacts, - state="predicting", - ) - predictions = _generate_predictions( - annotations, - config=config, - manifest=manifest, - chunk_pooling=chunk_pooling, - ) + config=config, + manifest=manifest, + chunk_pooling=chunk_pooling, + runtime=runtime, + storage=storage, + ) validate_predictions(predictions, annotations) predictions_path = run_directory / "predictions.json" ground_truth_path = run_directory / "ground_truth.subset.json" @@ -444,6 +493,9 @@ def run_didemo( "chunk_pooling": chunk_pooling, "media_override_count": len(media_overrides or {}), "media_override_video_ids": sorted(media_overrides or {}), + "media_id_mapping": ( + "deterministic_uuid4_from_benchmark_and_official_video_id" + ), "result_classification": _result_classification( split=split, full_split=annotation_indices is None, diff --git a/src/vidxp/benchmarks/hirest.py b/src/vidxp/benchmarks/hirest.py index ce499a3..15ece16 100644 --- a/src/vidxp/benchmarks/hirest.py +++ b/src/vidxp/benchmarks/hirest.py @@ -13,6 +13,8 @@ from vidxp.benchmarks.common import ( append_failure, + benchmark_generation_id, + benchmark_media_id, ensure_adapter_outputs, record_adapter_manifest, run_logged_evaluator, @@ -21,9 +23,14 @@ from vidxp.capabilities.dialogue.config import dialogue_config from vidxp.capabilities.dialogue.operations import search_dialogue from vidxp.capabilities.schemas import SearchHit +from vidxp.capabilities.registry import create_capability_registry from vidxp.core.contracts import IndexConfig, VideoSource -from vidxp.core.manifest import sha256_file, write_json_atomic +from vidxp.core.manifest import ManifestStore, sha256_file, write_json_atomic from vidxp.core.runner import run_index +from vidxp.core.storage import IndexStorage +from vidxp.infrastructure.local_index import LOCAL_INDEX_RUNTIME_CHECKS +from vidxp.runtime import ModelRuntime +from vidxp.settings import VidXPSettings HIREST_REVISION = "deffc169b4e8d51c1589d5512ad05da61e81bcee" @@ -46,6 +53,9 @@ "157623d50f7b8482f55fa1c4efc500539784c0399fb2dd60bb687b4006d85ca1" ) HIREST_EVALUATOR_SHA256 = ( + "871b48dc5ce42fbe1a4b672fe4df88a88ce568d57759dfc971e5aacc5f88f119" +) +HIREST_EVALUATOR_CRLF_SHA256 = ( "c4b8ba9b572ae4088e90ddc3eec2b2cc4f5b4c1a0153ff6e0843817da89a5ca0" ) HIREST_DEFAULT_WINDOW_FRACTION = 0.8 @@ -264,7 +274,10 @@ def _verified_artifacts( verify_artifact( evaluator_path, name="HiREST evaluator", - expected_sha256=HIREST_EVALUATOR_SHA256, + expected_sha256=( + HIREST_EVALUATOR_SHA256, + HIREST_EVALUATOR_CRLF_SHA256, + ), source=f"{revision_root}/evaluate.py", revision=HIREST_REVISION, ), @@ -292,7 +305,7 @@ def _transcript_sources( ) sources.append( VideoSource( - video_id=video, + video_id=benchmark_media_id("hirest", video), source_name=asr_path.name, transcript=parse_srt(asr_path), checksum=sha256_file(asr_path), @@ -312,6 +325,8 @@ def _generate_predictions( config: IndexConfig, manifest: Mapping[str, Any], temporal_window_fraction: float, + runtime: ModelRuntime, + storage: IndexStorage, ) -> dict[str, dict[str, dict[str, list[float]]]]: dialogue_counts = { video_id: int(video["summary"]["dialogue_phrases"]) @@ -319,12 +334,15 @@ def _generate_predictions( } predictions: dict[str, dict[str, dict[str, list[float]]]] = {} for prompt, video in ordered_pairs: + media_id = benchmark_media_id("hirest", video) hits = search_dialogue( prompt, config=config, - top_k=dialogue_counts[video], - video_id=video, + top_k=dialogue_counts[media_id], + video_id=media_id, query_id=f"{prompt}\0{video}", + runtime=runtime, + storage=storage, ).hits if not hits: raise RuntimeError( @@ -430,8 +448,19 @@ def run_hirest( enabled_modalities=("dialogue",), device=device, output_root=output_root, + generation_id=benchmark_generation_id("hirest", split, run_id), ) run_directory = config.run_directory + registry = create_capability_registry( + platform_runtime_checks=LOCAL_INDEX_RUNTIME_CHECKS + ) + runtime = ModelRuntime( + VidXPSettings( + repository_root=run_directory, + runtime_backend=device, + ), + allowed_specs=registry.model_specs(), + ) ensure_adapter_outputs(run_directory) subset = { "label": ( @@ -444,29 +473,40 @@ def run_hirest( "video_count": len({video for _, video in ordered_pairs}), } try: - manifest = run_index( - _transcript_sources( + with IndexStorage(config) as storage: + manifest = run_index( + _transcript_sources( + ordered_pairs, + asr_directory=asr_directory, + ), + config, + reset=reset, + storage=storage, + manifest_store=ManifestStore( + config, + registry=registry, + runtime=runtime, + ), + registry=registry, + runtime=runtime, + ) + ensure_adapter_outputs(run_directory) + record_adapter_manifest( + run_directory, + benchmark="hirest", + subset=subset, + artifacts=artifacts, + state="predicting", + ) + predictions = _generate_predictions( ordered_pairs, - asr_directory=asr_directory, - ), - config, - reset=reset, - ) - ensure_adapter_outputs(run_directory) - record_adapter_manifest( - run_directory, - benchmark="hirest", - subset=subset, - artifacts=artifacts, - state="predicting", - ) - predictions = _generate_predictions( - ordered_pairs, - ground_truth=ground_truth, - config=config, - manifest=manifest, - temporal_window_fraction=temporal_window_fraction, - ) + ground_truth=ground_truth, + config=config, + manifest=manifest, + temporal_window_fraction=temporal_window_fraction, + runtime=runtime, + storage=storage, + ) validate_predictions(predictions, ground_truth) predictions_path = run_directory / "predictions.json" ground_truth_subset_path = ( @@ -489,8 +529,11 @@ def run_hirest( "duration_relative_window_mean_second_score" ), "temporal_window_fraction": temporal_window_fraction, - "whisperx_used": False, + "transcription_provider": "supplied-transcript", "video_decode_used": False, + "media_id_mapping": ( + "deterministic_uuid4_from_benchmark_and_official_video_id" + ), } if split == "test": summary = { diff --git a/src/vidxp/benchmarks/prepare.py b/src/vidxp/benchmarks/prepare.py new file mode 100644 index 0000000..3e861df --- /dev/null +++ b/src/vidxp/benchmarks/prepare.py @@ -0,0 +1,879 @@ +from __future__ import annotations + +import hashlib +import json +import os +import subprocess +import tempfile +import urllib.error +import urllib.request +import zipfile +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Any, Callable, Literal, Mapping, Sequence + +from vidxp.application_models import ApplicationError, ErrorCategory +from vidxp.benchmarks.didemo import ( + DIDEMO_EVALUATOR_SHA256, + DIDEMO_REVISION, + DIDEMO_TEST_SHA256, + DIDEMO_VALIDATION_SHA256, + parse_annotations, + select_annotations, +) +from vidxp.benchmarks.hirest import ( + HIREST_ASR_SHA256, + HIREST_ASR_URL, + HIREST_CATEGORIES_SHA256, + HIREST_EVALUATOR_SHA256, + HIREST_REVISION, + HIREST_TEST_SHA256, + HIREST_VALIDATION_SHA256, + select_ground_truth, +) +from vidxp.core.manifest import sha256_file, utc_now, write_json_atomic +from vidxp.infrastructure.local_media import FFprobeMediaProbe +from vidxp.media_runtime import inspect_media_runtime + + +DIDEMO_RAW_ROOT = ( + "https://raw.githubusercontent.com/LisaAnne/LocalizingMoments/" + f"{DIDEMO_REVISION}" +) +DIDEMO_HASH_MAP_SHA256 = ( + "481d9aaf020624d5915200bcf4752fb46d3e1931167e8b46715a5f342577cc4d" +) +DIDEMO_AWS_TEMPLATE = ( + "https://multimedia-commons.s3-us-west-2.amazonaws.com/" + "data/videos/mp4/{first}/{second}/{digest}.mp4" +) +DIDEMO_KNOWN_REPLACEMENT_VIDEO = ( + "12090392@N02_13482799053_87ef417396.mov" +) +DIDEMO_KNOWN_REPLACEMENT_URL = ( + "https://upload.wikimedia.org/wikipedia/commons/a/af/" + "Common_Starlings_flying_away_from_a_Marsh_Harrier.webm" +) +DIDEMO_KNOWN_REPLACEMENT_SHA1 = ( + "2aefa90d4256e74cf62e492729c0e0f6d6bede72" +) +DIDEMO_KNOWN_REPLACEMENT_SIZE = 94_107_862 + +HIREST_RAW_ROOT = ( + "https://raw.githubusercontent.com/j-min/HiREST/" + f"{HIREST_REVISION}" +) +HIREST_ASR_UNCOMPRESSED_SIZE = 17_236_665 + +ProgressCallback = Callable[[str, int], None] + + +@dataclass(frozen=True) +class PreparationResource: + name: str + url: str + destination: Path + size_bytes: int + kind: Literal["artifact", "media"] + content: bytes | None = None + expected_sha256: str | None = None + expected_sha1: str | None = None + existing: bool = False + replacement_for: str | None = None + + def public_record(self) -> dict[str, Any]: + return { + "name": self.name, + "url": self.url, + "path": str(self.destination), + "size_bytes": self.size_bytes, + "kind": self.kind, + "expected_sha256": self.expected_sha256, + "expected_sha1": self.expected_sha1, + "existing": self.existing, + "replacement_for": self.replacement_for, + } + + +@dataclass(frozen=True) +class PreparationPlan: + benchmark: Literal["didemo", "hirest"] + split: Literal["validation", "test"] + root: Path + resources: tuple[PreparationResource, ...] + selected_count: int + selected_video_names: tuple[str, ...] + command: str + manifest_path: Path + media_directory: Path | None = None + media_overrides_path: Path | None = None + asr_directory: Path | None = None + extraction_reserve_bytes: int = 0 + + @property + def additional_bytes(self) -> int: + return self.extraction_reserve_bytes + sum( + self._remaining_bytes(resource) for resource in self.resources + ) + + @property + def network_bytes(self) -> int: + return sum( + self._remaining_bytes(resource) + for resource in self.resources + if resource.content is None + ) + + @property + def download_count(self) -> int: + return sum(not resource.existing for resource in self.resources) + + @staticmethod + def _remaining_bytes(resource: PreparationResource) -> int: + if resource.existing: + return 0 + if resource.content is not None: + return resource.size_bytes + partial = resource.destination.with_name( + resource.destination.name + ".part" + ) + partial_size = partial.stat().st_size if partial.is_file() else 0 + if partial_size > resource.size_bytes: + return resource.size_bytes + return resource.size_bytes - partial_size + + +def _application_error( + code: str, + message: str, + *, + details: Mapping[str, Any] | None = None, +) -> ApplicationError: + return ApplicationError( + code, + ErrorCategory.unavailable, + message, + details=dict(details or {}), + ) + + +def _request(url: str, *, method: str = "GET", range_start: int = 0): + headers = {"User-Agent": "VidXP benchmark preparation"} + if range_start: + headers["Range"] = f"bytes={range_start}-" + return urllib.request.Request(url, headers=headers, method=method) + + +def _fetch_bytes(url: str) -> bytes: + try: + with urllib.request.urlopen(_request(url), timeout=60) as response: + return response.read() + except (OSError, urllib.error.URLError) as exc: + raise _application_error( + "benchmark_source_unavailable", + f"Could not read pinned benchmark source: {url}", + details={"url": url, "reason": str(exc)}, + ) from exc + + +def _fetch_verified(url: str, expected_sha256: str, name: str) -> bytes: + content = _fetch_bytes(url) + observed = hashlib.sha256(content).hexdigest() + if observed != expected_sha256: + raise _application_error( + "benchmark_source_checksum_mismatch", + f"{name} did not match its pinned SHA-256.", + details={ + "url": url, + "expected_sha256": expected_sha256, + "observed_sha256": observed, + }, + ) + return content + + +def _remote_size(url: str) -> int: + try: + with urllib.request.urlopen( + _request(url, method="HEAD"), + timeout=30, + ) as response: + value = response.headers.get("Content-Length") + if value and int(value) > 0: + return int(value) + except (OSError, ValueError, urllib.error.URLError): + pass + try: + with urllib.request.urlopen( + _request(url, range_start=0), + timeout=30, + ) as response: + content_range = response.headers.get("Content-Range", "") + if "/" in content_range: + value = int(content_range.rsplit("/", 1)[1]) + if value > 0: + return value + value = response.headers.get("Content-Length") + if value and int(value) > 0: + return int(value) + except (OSError, ValueError, urllib.error.URLError) as exc: + raise _application_error( + "benchmark_download_size_unavailable", + f"Could not determine the download size for {url}", + details={"url": url, "reason": str(exc)}, + ) from exc + raise _application_error( + "benchmark_download_size_unavailable", + f"The benchmark source did not report a download size: {url}", + details={"url": url}, + ) + + +def _sha1_file(path: Path) -> str: + digest = hashlib.sha1() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _valid_artifact( + path: Path, + *, + expected_sha256: str | None = None, + expected_sha1: str | None = None, +) -> bool: + if not path.is_file(): + return False + if expected_sha256 and sha256_file(path) != expected_sha256: + return False + return not expected_sha1 or _sha1_file(path) == expected_sha1 + + +def _validate_media(path: Path, *, ffprobe: str, ffmpeg: str) -> None: + media = FFprobeMediaProbe(ffprobe).probe(path) + seek = min(15.0, max(0.0, media.duration_seconds / 2)) + try: + subprocess.run( + [ + ffmpeg, + "-v", + "error", + "-ss", + str(seek), + "-i", + str(path), + "-frames:v", + "1", + "-f", + "null", + "-", + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + timeout=60, + ) + except OSError as exc: + raise _application_error( + "benchmark_media_runtime_unavailable", + "FFmpeg could not be executed while validating benchmark media.", + details={"ffmpeg": ffmpeg, "reason": str(exc)}, + ) from exc + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: + raise ValueError( + f"FFmpeg could not decode a frame from {path}." + ) from exc + + +def _valid_media(path: Path, *, ffprobe: str, ffmpeg: str) -> bool: + if not path.is_file(): + return False + try: + _validate_media(path, ffprobe=ffprobe, ffmpeg=ffmpeg) + except (ApplicationError, OSError, ValueError): + return False + return True + + +def _artifact_resource( + *, + name: str, + url: str, + destination: Path, + expected_sha256: str, + content: bytes, +) -> PreparationResource: + return PreparationResource( + name=name, + url=url, + destination=destination, + size_bytes=len(content), + kind="artifact", + content=content, + expected_sha256=expected_sha256, + existing=_valid_artifact( + destination, + expected_sha256=expected_sha256, + ), + ) + + +def _with_remote_sizes( + resources: Sequence[PreparationResource], +) -> tuple[PreparationResource, ...]: + resolved = list(resources) + pending = { + index: resource + for index, resource in enumerate(resources) + if ( + resource.content is None + and not resource.existing + and resource.size_bytes <= 0 + ) + } + with ThreadPoolExecutor(max_workers=8) as pool: + futures = { + pool.submit(_remote_size, resource.url): index + for index, resource in pending.items() + } + for future in as_completed(futures): + index = futures[future] + resolved[index] = replace( + pending[index], + size_bytes=future.result(), + ) + return tuple(resolved) + + +def _parse_didemo_hashes(content: bytes) -> dict[str, str]: + mapping: dict[str, str] = {} + for line in content.decode("utf-8").splitlines(): + fields = line.strip().split("\t") + if len(fields) == 2 and fields[0] and fields[1]: + mapping[fields[0]] = fields[1] + if not mapping: + raise ValueError("The pinned DiDeMo YFCC hash map is empty.") + return mapping + + +def _didemo_aws_url(video_name: str, hashes: Mapping[str, str]) -> str: + fields = video_name.split("_") + if len(fields) < 3 or not fields[1]: + raise ValueError(f"Unsupported DiDeMo video name: {video_name}") + try: + digest = hashes[fields[1]] + except KeyError as exc: + raise ValueError( + f"No YFCC hash exists for DiDeMo video {video_name}." + ) from exc + return DIDEMO_AWS_TEMPLATE.format( + first=digest[:3], + second=digest[3:6], + digest=digest, + ) + + +def _quote_powershell(value: Path | str) -> str: + return "'" + str(value).replace("'", "''") + "'" + + +def plan_didemo( + *, + root: str | Path, + split: Literal["validation", "test"], + annotation_indices: Sequence[int] | None, + ffprobe: str, + ffmpeg: str, +) -> PreparationPlan: + runtime_status = inspect_media_runtime( + ffprobe=ffprobe, + ffmpeg=ffmpeg, + ) + if not runtime_status.ready: + raise _application_error( + "benchmark_media_runtime_unavailable", + "DiDeMo preparation requires FFmpeg and ffprobe to validate " + "downloaded videos. Run `vidxp init`, then retry.", + details={ + "errors": list(runtime_status.errors), + "remediation": "vidxp init", + }, + ) + destination = Path(root).expanduser().resolve() + split_name = "val" if split == "validation" else "test" + annotation_sha = ( + DIDEMO_VALIDATION_SHA256 + if split == "validation" + else DIDEMO_TEST_SHA256 + ) + annotation_url = f"{DIDEMO_RAW_ROOT}/data/{split_name}_data.json" + evaluator_url = f"{DIDEMO_RAW_ROOT}/utils/eval.py" + hash_url = f"{DIDEMO_RAW_ROOT}/data/yfcc100m_hash.txt" + annotation_content = _fetch_verified( + annotation_url, + annotation_sha, + f"DiDeMo {split} annotations", + ) + evaluator_content = _fetch_verified( + evaluator_url, + DIDEMO_EVALUATOR_SHA256, + "DiDeMo evaluator", + ) + hash_content = _fetch_verified( + hash_url, + DIDEMO_HASH_MAP_SHA256, + "DiDeMo YFCC hash map", + ) + annotations = select_annotations( + parse_annotations(json.loads(annotation_content)), + annotation_indices, + ) + video_names = tuple( + sorted({str(annotation["video"]) for annotation in annotations}) + ) + hashes = _parse_didemo_hashes(hash_content) + annotations_path = destination / "annotations" / f"{split_name}_data.json" + evaluator_path = destination / "evaluator" / "eval.py" + hash_path = destination / "metadata" / "yfcc100m_hash.txt" + media_directory = destination / "media" + resources: list[PreparationResource] = [ + _artifact_resource( + name=f"DiDeMo {split} annotations", + url=annotation_url, + destination=annotations_path, + expected_sha256=annotation_sha, + content=annotation_content, + ), + _artifact_resource( + name="DiDeMo evaluator", + url=evaluator_url, + destination=evaluator_path, + expected_sha256=DIDEMO_EVALUATOR_SHA256, + content=evaluator_content, + ), + _artifact_resource( + name="DiDeMo YFCC hash map", + url=hash_url, + destination=hash_path, + expected_sha256=DIDEMO_HASH_MAP_SHA256, + content=hash_content, + ), + ] + overrides: dict[str, str] = {} + for video_name in video_names: + if video_name == DIDEMO_KNOWN_REPLACEMENT_VIDEO: + replacement_path = ( + destination + / "replacements" + / "common-starlings-flickr-13482799053.webm" + ) + overrides[video_name] = str( + replacement_path.relative_to(destination) + ) + resources.append( + PreparationResource( + name=f"documented replacement for {video_name}", + url=DIDEMO_KNOWN_REPLACEMENT_URL, + destination=replacement_path, + size_bytes=DIDEMO_KNOWN_REPLACEMENT_SIZE, + kind="media", + expected_sha1=DIDEMO_KNOWN_REPLACEMENT_SHA1, + existing=( + _valid_artifact( + replacement_path, + expected_sha1=DIDEMO_KNOWN_REPLACEMENT_SHA1, + ) + and _valid_media( + replacement_path, + ffprobe=ffprobe, + ffmpeg=ffmpeg, + ) + ), + replacement_for=video_name, + ) + ) + continue + path = media_directory / video_name + resources.append( + PreparationResource( + name=video_name, + url=_didemo_aws_url(video_name, hashes), + destination=path, + size_bytes=0, + kind="media", + existing=_valid_media( + path, + ffprobe=ffprobe, + ffmpeg=ffmpeg, + ), + ) + ) + overrides_path = ( + destination / "media-overrides.json" if overrides else None + ) + if overrides_path is not None: + override_content = ( + json.dumps(overrides, indent=2, sort_keys=True) + "\n" + ).encode("utf-8") + override_sha = hashlib.sha256(override_content).hexdigest() + resources.append( + _artifact_resource( + name="DiDeMo media overrides", + url="documented-local-manifest", + destination=overrides_path, + expected_sha256=override_sha, + content=override_content, + ) + ) + resources = list(_with_remote_sizes(resources)) + command = ( + "vidxp benchmark didemo " + f"--annotations {_quote_powershell(annotations_path)} " + f"--evaluator {_quote_powershell(evaluator_path)} " + f"--media-directory {_quote_powershell(media_directory)} " + f"--split {split} " + ) + if annotation_indices is not None: + command += ( + "--annotation-indices " + + ",".join(str(index) for index in annotation_indices) + + " " + ) + if overrides_path is not None: + command += ( + f"--media-overrides {_quote_powershell(overrides_path)} " + ) + command += f"--run-id didemo-{split}-run" + return PreparationPlan( + benchmark="didemo", + split=split, + root=destination, + resources=tuple(resources), + selected_count=len(annotations), + selected_video_names=video_names, + command=command, + manifest_path=destination / "preparation-manifest.json", + media_directory=media_directory, + media_overrides_path=overrides_path, + ) + + +def plan_hirest( + *, + root: str | Path, + split: Literal["validation", "test"], + pairs: Sequence[tuple[str, str]] | None, +) -> PreparationPlan: + destination = Path(root).expanduser().resolve() + split_name = "val" if split == "validation" else "test" + ground_truth_sha = ( + HIREST_VALIDATION_SHA256 + if split == "validation" + else HIREST_TEST_SHA256 + ) + ground_truth_url = ( + f"{HIREST_RAW_ROOT}/data/splits/all_data_{split_name}.json" + ) + categories_url = f"{HIREST_RAW_ROOT}/data/evaluation/categories.json" + evaluator_url = f"{HIREST_RAW_ROOT}/evaluate.py" + ground_truth_content = _fetch_verified( + ground_truth_url, + ground_truth_sha, + f"HiREST {split} ground truth", + ) + categories_content = _fetch_verified( + categories_url, + HIREST_CATEGORIES_SHA256, + "HiREST categories", + ) + evaluator_content = _fetch_verified( + evaluator_url, + HIREST_EVALUATOR_SHA256, + "HiREST evaluator", + ) + ground_truth = json.loads(ground_truth_content) + _, ordered_pairs = select_ground_truth(ground_truth, pairs) + video_names = tuple(sorted({video for _, video in ordered_pairs})) + ground_truth_path = ( + destination / "annotations" / f"all_data_{split_name}.json" + ) + categories_path = destination / "evaluator" / "categories.json" + evaluator_path = destination / "evaluator" / "evaluate.py" + asr_archive_path = destination / "ASR.zip" + asr_directory = destination / "asr" + resources = [ + _artifact_resource( + name=f"HiREST {split} ground truth", + url=ground_truth_url, + destination=ground_truth_path, + expected_sha256=ground_truth_sha, + content=ground_truth_content, + ), + _artifact_resource( + name="HiREST categories", + url=categories_url, + destination=categories_path, + expected_sha256=HIREST_CATEGORIES_SHA256, + content=categories_content, + ), + _artifact_resource( + name="HiREST evaluator", + url=evaluator_url, + destination=evaluator_path, + expected_sha256=HIREST_EVALUATOR_SHA256, + content=evaluator_content, + ), + PreparationResource( + name="HiREST released ASR", + url=HIREST_ASR_URL, + destination=asr_archive_path, + size_bytes=0, + kind="artifact", + expected_sha256=HIREST_ASR_SHA256, + existing=_valid_artifact( + asr_archive_path, + expected_sha256=HIREST_ASR_SHA256, + ), + ), + ] + pairs_path = None + if pairs is not None: + pairs_path = destination / "selection" / "pairs.json" + pairs_content = ( + json.dumps( + [ + {"prompt": prompt, "video": video} + for prompt, video in pairs + ], + indent=2, + ensure_ascii=False, + ) + + "\n" + ).encode("utf-8") + pairs_sha = hashlib.sha256(pairs_content).hexdigest() + resources.append( + _artifact_resource( + name="HiREST selected pairs", + url="user-declared-selection", + destination=pairs_path, + expected_sha256=pairs_sha, + content=pairs_content, + ) + ) + resources = list(_with_remote_sizes(resources)) + command = ( + "vidxp benchmark hirest " + f"--ground-truth {_quote_powershell(ground_truth_path)} " + f"--categories {_quote_powershell(categories_path)} " + f"--evaluator {_quote_powershell(evaluator_path)} " + f"--asr-archive {_quote_powershell(asr_archive_path)} " + f"--asr-directory {_quote_powershell(asr_directory)} " + f"--split {split} " + ) + if pairs_path is not None: + command += f"--pairs {_quote_powershell(pairs_path)} " + command += f"--run-id hirest-{split}-run" + transcripts_ready = all( + (asr_directory / f"{Path(video).stem}.srt").is_file() + and (asr_directory / f"{Path(video).stem}.srt").stat().st_size > 0 + for video in video_names + ) + return PreparationPlan( + benchmark="hirest", + split=split, + root=destination, + resources=tuple(resources), + selected_count=len(ordered_pairs), + selected_video_names=video_names, + command=command, + manifest_path=destination / "preparation-manifest.json", + asr_directory=asr_directory, + extraction_reserve_bytes=( + 0 if transcripts_ready else HIREST_ASR_UNCOMPRESSED_SIZE + ), + ) + + +def _download_resource( + resource: PreparationResource, + *, + ffprobe: str, + ffmpeg: str, + progress: ProgressCallback | None, +) -> str: + if resource.existing: + return "reused" + resource.destination.parent.mkdir(parents=True, exist_ok=True) + if resource.content is not None: + temporary = resource.destination.with_name( + resource.destination.name + ".part" + ) + temporary.write_bytes(resource.content) + else: + temporary = resource.destination.with_name( + resource.destination.name + ".part" + ) + start = temporary.stat().st_size if temporary.is_file() else 0 + if start > resource.size_bytes: + temporary.unlink() + start = 0 + if start < resource.size_bytes: + request = _request(resource.url, range_start=start) + try: + with urllib.request.urlopen(request, timeout=60) as response: + status = getattr(response, "status", response.getcode()) + if start and status != 206: + start = 0 + mode = "ab" if start and status == 206 else "wb" + with temporary.open(mode) as stream: + while chunk := response.read(1024 * 1024): + stream.write(chunk) + if progress is not None: + progress(resource.name, len(chunk)) + except (OSError, urllib.error.URLError) as exc: + raise _application_error( + "benchmark_download_failed", + f"Download failed for {resource.name}.", + details={ + "url": resource.url, + "partial_path": str(temporary), + "reason": str(exc), + }, + ) from exc + if temporary.stat().st_size != resource.size_bytes: + raise ValueError( + f"{resource.name} has {temporary.stat().st_size} bytes; " + f"expected {resource.size_bytes}." + ) + if resource.expected_sha256: + observed = sha256_file(temporary) + if observed != resource.expected_sha256: + raise ValueError( + f"{resource.name} SHA-256 was {observed}; " + f"expected {resource.expected_sha256}." + ) + if resource.expected_sha1: + observed = _sha1_file(temporary) + if observed != resource.expected_sha1: + raise ValueError( + f"{resource.name} SHA-1 was {observed}; " + f"expected {resource.expected_sha1}." + ) + if resource.kind == "media": + _validate_media(temporary, ffprobe=ffprobe, ffmpeg=ffmpeg) + os.replace(temporary, resource.destination) + return "downloaded" + + +def _extract_hirest_asr( + archive: Path, + destination: Path, + video_names: Sequence[str], +) -> int: + destination.mkdir(parents=True, exist_ok=True) + expected = {f"{Path(video).stem}.srt" for video in video_names} + extracted = 0 + with zipfile.ZipFile(archive) as bundle: + members: dict[str, zipfile.ZipInfo] = {} + for info in bundle.infolist(): + name = Path(info.filename).name + if name in expected: + if name in members: + raise ValueError( + f"HiREST ASR archive contains duplicate {name}." + ) + members[name] = info + missing = sorted(expected - set(members)) + if missing: + raise ValueError( + "HiREST ASR archive is missing selected transcripts: " + + ", ".join(missing[:10]) + ) + for name, info in members.items(): + target = destination / name + if target.is_file() and target.stat().st_size > 0: + continue + with ( + bundle.open(info) as source, + tempfile.NamedTemporaryFile( + dir=destination, + prefix=name + ".", + suffix=".part", + delete=False, + ) as temporary, + ): + while chunk := source.read(1024 * 1024): + temporary.write(chunk) + temporary_path = Path(temporary.name) + os.replace(temporary_path, target) + extracted += 1 + return extracted + + +def execute_preparation( + plan: PreparationPlan, + *, + ffprobe: str = "ffprobe", + ffmpeg: str = "ffmpeg", + progress: ProgressCallback | None = None, +) -> dict[str, Any]: + plan.root.mkdir(parents=True, exist_ok=True) + outcomes: dict[str, str] = {} + failures: list[dict[str, str]] = [] + for resource in plan.resources: + try: + outcomes[resource.name] = _download_resource( + resource, + ffprobe=ffprobe, + ffmpeg=ffmpeg, + progress=progress, + ) + except Exception as exc: + failures.append( + {"resource": resource.name, "error": str(exc)} + ) + extracted = 0 + if not failures and plan.benchmark == "hirest": + archive = next( + resource.destination + for resource in plan.resources + if resource.expected_sha256 == HIREST_ASR_SHA256 + ) + try: + extracted = _extract_hirest_asr( + archive, + plan.asr_directory or plan.root / "asr", + plan.selected_video_names, + ) + except Exception as exc: + failures.append({"resource": "HiREST ASR extraction", "error": str(exc)}) + manifest = { + "schema_version": 1, + "status": "failed" if failures else "ready", + "created_at": utc_now(), + "benchmark": plan.benchmark, + "split": plan.split, + "root": str(plan.root), + "selected_count": plan.selected_count, + "selected_video_count": len(plan.selected_video_names), + "planned_max_additional_bytes": plan.additional_bytes, + "resources": [resource.public_record() for resource in plan.resources], + "outcomes": outcomes, + "failures": failures, + "asr_transcripts_extracted": extracted, + "command": plan.command, + } + write_json_atomic(plan.manifest_path, manifest) + if failures: + raise _application_error( + "benchmark_preparation_failed", + "Benchmark preparation did not complete. Partial downloads were " + "kept for a resumable retry.", + details={ + "manifest": str(plan.manifest_path), + "failures": failures, + }, + ) + return manifest diff --git a/src/vidxp/branding.py b/src/vidxp/branding.py new file mode 100644 index 0000000..9abae12 --- /dev/null +++ b/src/vidxp/branding.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import base64 +from functools import lru_cache +from importlib.resources import files +from pathlib import Path + + +PROJECT_URL = "https://github.com/grayhatdevelopers/vidxp" +ICON_MIME_TYPE = "image/png" +ICON_SIZE = "303x303" + + +def icon_path() -> str: + """Return the canonical icon in source or its installed wheel copy.""" + + packaged_icon = files("vidxp").joinpath("assets", "icon.png") + if packaged_icon.is_file(): + return str(packaged_icon) + + source_icon = ( + Path(__file__).resolve().parents[2] / "docs" / "images" / "logo.png" + ) + if source_icon.is_file(): + return str(source_icon) + raise FileNotFoundError("VidXP icon is missing from the installation.") + + +@lru_cache(maxsize=1) +def icon_bytes() -> bytes: + return Path(icon_path()).read_bytes() + + +@lru_cache(maxsize=1) +def icon_data_uri() -> str: + encoded = base64.b64encode(icon_bytes()).decode("ascii") + return f"data:{ICON_MIME_TYPE};base64,{encoded}" diff --git a/src/vidxp/capabilities/__init__.py b/src/vidxp/capabilities/__init__.py index 381c275..5902c1f 100644 --- a/src/vidxp/capabilities/__init__.py +++ b/src/vidxp/capabilities/__init__.py @@ -1,15 +1,11 @@ -"""Built-in VidXP capabilities and their explicit registry.""" +"""Capability contracts and registry construction.""" from vidxp.capabilities.registry import ( - CAPABILITIES, - capability_names, - get_capability, - index_capability_names, + CapabilityRegistry, + create_capability_registry, ) __all__ = [ - "CAPABILITIES", - "capability_names", - "get_capability", - "index_capability_names", + "CapabilityRegistry", + "create_capability_registry", ] diff --git a/src/vidxp/capabilities/actor/config.py b/src/vidxp/capabilities/actor/config.py index 8756fd0..9a11ae7 100644 --- a/src/vidxp/capabilities/actor/config.py +++ b/src/vidxp/capabilities/actor/config.py @@ -8,8 +8,8 @@ class ActorConfig(CapabilityConfig): batch_size: int = Field(default=16, gt=0) - match_threshold: float = Field(default=0.55, gt=0, lt=1) - num_jitters: int = Field(default=2, gt=0) + match_threshold: float = Field(default=0.363, gt=0, lt=1) + detection_threshold: float = Field(default=0.9, gt=0, lt=1) minimum_detections: int = Field(default=4, gt=0) diff --git a/src/vidxp/capabilities/actor/definition.py b/src/vidxp/capabilities/actor/definition.py index 690005d..f8e1e1c 100644 --- a/src/vidxp/capabilities/actor/definition.py +++ b/src/vidxp/capabilities/actor/definition.py @@ -4,25 +4,51 @@ from vidxp.capabilities.actor.config import ActorConfig, actor_config from vidxp.capabilities.actor.indexing import VISUAL_PROCESSOR +from vidxp.capabilities.actor.models import ( + get_actor_models, +) from vidxp.capabilities.actor.operations import ( + cluster_operation, clusters_operation, detections_operation, - render_operation, ) +from vidxp.capabilities.actor.specs import SFACE_MODEL, YUNET_MODEL from vidxp.capabilities.contracts import ( CapabilityDefinition, + CapabilityExecutor, + CapabilityPlugin, OperationDefinition, + PreparationContext, + module_import_check, ) from vidxp.capabilities.actor.schemas import ( + ActorClusterInput, + ActorClusterSummary, ActorClustersInput, ActorClustersOutput, ActorDetectionsInput, ActorDetectionsOutput, - ActorRenderInput, - ActorRenderResult, ) from vidxp.capabilities.visual import index_capabilities from vidxp.core.contracts import IndexConfig, VideoSource +from vidxp.core.indexing_common import ProgressCallback + + +def prepare_models( + context: PreparationContext, + progress: ProgressCallback | None, +) -> tuple[str, ...]: + if progress is not None: + progress( + { + "state": "preparing", + "stage": "actor_models", + "message": "Preparing OpenCV Zoo YuNet and SFace models.", + } + ) + get_actor_models(context.runtime, download=True, progress=progress) + return (YUNET_MODEL.filename, SFACE_MODEL.filename) + def model_manifest( config: IndexConfig, @@ -31,47 +57,69 @@ def model_manifest( settings = actor_config(config) return { "actor": { - "library": "face_recognition", + "models": { + "detector": YUNET_MODEL.identity(), + "recognizer": SFACE_MODEL.identity(), + }, "match_threshold": settings.match_threshold, - "num_jitters": settings.num_jitters, + "detection_threshold": settings.detection_threshold, "minimum_detections": settings.minimum_detections, } } -def cli_app(): - from vidxp.capabilities.actor.cli import app - - return app - - DEFINITION = CapabilityDefinition( name="actor", description="Index, inspect, and render actor clusters.", extra="actor", config_model=ActorConfig, collection_name="actor", - indexer=index_capabilities, - index_processor=VISUAL_PROCESSOR, index_stage="visual_indexing", - model_manifest=model_manifest, + execution_group="visual", + prepares_models=True, + model_specs=(YUNET_MODEL, SFACE_MODEL), operations={ + "cluster": OperationDefinition( + input_model=ActorClusterInput, + output_model=ActorClusterSummary, + public=False, + ), "clusters": OperationDefinition( input_model=ActorClustersInput, output_model=ActorClustersOutput, - handler=clusters_operation, ), "detections": OperationDefinition( input_model=ActorDetectionsInput, output_model=ActorDetectionsOutput, - handler=detections_operation, - ), - "render": OperationDefinition( - input_model=ActorRenderInput, - output_model=ActorRenderResult, - handler=render_operation, ), }, - cli_name="actors", - cli_factory=cli_app, +) + + +def create_executor() -> CapabilityExecutor: + return CapabilityExecutor( + indexer=index_capabilities, + index_processor=VISUAL_PROCESSOR, + operations={ + "cluster": cluster_operation, + "clusters": clusters_operation, + "detections": detections_operation, + }, + model_manifest=model_manifest, + prepare=prepare_models, + runtime_checks=( + module_import_check( + "OpenCV import", + "cv2", + "FaceDetectorYN", + "FaceRecognizerSF", + ), + module_import_check("Pooch import", "pooch", "retrieve"), + ), + ) + + +PLUGIN = CapabilityPlugin( + definition=DEFINITION, + executor_factory=create_executor, ) diff --git a/src/vidxp/capabilities/actor/indexing.py b/src/vidxp/capabilities/actor/indexing.py index 7328d10..35da06c 100644 --- a/src/vidxp/capabilities/actor/indexing.py +++ b/src/vidxp/capabilities/actor/indexing.py @@ -4,6 +4,7 @@ from typing import Any from vidxp.capabilities.actor.config import actor_config +from vidxp.capabilities.actor.models import ActorModels, get_actor_models from vidxp.core.contracts import ( CancellationToken, IndexConfig, @@ -12,29 +13,48 @@ stable_source_id, ) from vidxp.core.indexing_common import ProgressCallback -from vidxp.core.storage import IndexStorage +from vidxp.core.video import FrameSampling +from vidxp.ports import IndexStore, ModelRuntimePort @dataclass class ActorIndexState: + models: ActorModels known_encodings: list[Any] = field(default_factory=list) known_ids: list[str] = field(default_factory=list) histories: dict[str, list[Any]] = field(default_factory=dict) cluster_sizes: dict[str, int] = field(default_factory=dict) + cluster_ranges: dict[str, tuple[float, float]] = field( + default_factory=dict + ) processed_frames: int = 0 -def _best_face_match( - face_recognition, - known_encodings, - encoding, - threshold, -): +def _best_face_match(known_encodings, encoding, threshold): + import numpy as np + if not known_encodings: return None - distances = face_recognition.face_distance(known_encodings, encoding) - match = int(distances.argmin()) - return match if distances[match] < threshold else None + similarities = np.asarray(known_encodings) @ encoding + match = int(similarities.argmax()) + return match if similarities[match] >= threshold else None + + +def _actor_cluster_id( + config: IndexConfig, + local_cluster_id: str | int, +) -> str: + if config.video_id is None: + raise ValueError( + "IndexConfig.video_id is required for actor cluster identity." + ) + return stable_source_id( + config.run_id, + config.video_id, + "actor-cluster", + local_cluster_id, + generation_id=config.generation_id, + ) def _actor_records( @@ -48,6 +68,7 @@ def _actor_records( str(config.video_id), "actor", detection["detection_id"], + generation_id=config.generation_id, ) top, right, bottom, left = detection["bbox"] records.append( @@ -70,15 +91,48 @@ def _actor_records( return records +def _actor_cluster_records( + cluster_sizes: dict[str, int], + cluster_ranges: dict[str, tuple[float, float]], + config: IndexConfig, +) -> list[StorageRecord]: + records = [] + for cluster_id in sorted(cluster_sizes): + size = cluster_sizes[cluster_id] + first_timestamp, last_timestamp = cluster_ranges[cluster_id] + source_id = stable_source_id( + config.run_id, + str(config.video_id), + "actor-cluster-summary", + cluster_id, + generation_id=config.generation_id, + ) + records.append( + StorageRecord( + source_id=source_id, + embedding=[0.0], + metadata={ + **config.record_identity("actor", source_id), + "record_kind": "cluster_summary", + "summary_cluster_id": cluster_id, + "detection_count": size, + "first_timestamp": first_timestamp, + "last_timestamp": last_timestamp, + }, + ) + ) + return records + + def process_actor_samples( samples, *, state: ActorIndexState, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, ) -> None: - import face_recognition + import cv2 import numpy as np settings = actor_config(config) @@ -87,23 +141,34 @@ def process_actor_samples( detections = [] for sample in group: cancellation.raise_if_cancelled() - locations = face_recognition.face_locations(sample.frame) - encodings = face_recognition.face_encodings( - sample.frame, - locations, - num_jitters=settings.num_jitters, + frame = cv2.cvtColor(sample.frame, cv2.COLOR_RGB2BGR) + height, width = frame.shape[:2] + state.models.detector.setInputSize((width, height)) + state.models.detector.setScoreThreshold( + settings.detection_threshold ) - for ordinal, (encoding, location) in enumerate( - zip(encodings, locations) - ): + _, faces = state.models.detector.detect(frame) + for ordinal, face in enumerate(faces if faces is not None else ()): + aligned = state.models.recognizer.alignCrop(frame, face) + encoding = ( + state.models.recognizer.feature(aligned) + .flatten() + .astype("float32") + ) + norm = float(np.linalg.norm(encoding)) + if norm == 0: + continue + encoding /= norm match = _best_face_match( - face_recognition, state.known_encodings, encoding, settings.match_threshold, ) if match is None: - cluster_id = str(len(state.known_ids) + 1) + cluster_id = _actor_cluster_id( + config, + len(state.known_ids) + 1, + ) state.known_ids.append(cluster_id) state.known_encodings.append(encoding) state.histories[cluster_id] = [encoding] @@ -113,10 +178,23 @@ def process_actor_samples( history.append(encoding) if len(history) > 5: history.pop(0) - state.known_encodings[match] = np.mean(history, axis=0) + centroid = np.mean(history, axis=0) + state.known_encodings[match] = ( + centroid / np.linalg.norm(centroid) + ) state.cluster_sizes[cluster_id] = ( state.cluster_sizes.get(cluster_id, 0) + 1 ) + previous_range = state.cluster_ranges.get(cluster_id) + timestamp = float(sample.timestamp) + state.cluster_ranges[cluster_id] = ( + timestamp + if previous_range is None + else min(previous_range[0], timestamp), + timestamp + if previous_range is None + else max(previous_range[1], timestamp), + ) detections.append( { "detection_id": ( @@ -125,7 +203,12 @@ def process_actor_samples( "cluster_id": cluster_id, "frame_index": sample.frame_index, "timestamp": sample.timestamp, - "bbox": tuple(int(value) for value in location), + "bbox": ( + max(0, int(face[1])), + min(width, int(face[0] + face[2])), + min(height, int(face[1] + face[3])), + max(0, int(face[0])), + ), } ) state.processed_frames += 1 @@ -141,7 +224,7 @@ def finalize_actor_index( state: ActorIndexState, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, ) -> tuple[int, int]: settings = actor_config(config) rejected = [ @@ -160,19 +243,36 @@ def finalize_actor_index( for cluster_id, size in state.cluster_sizes.items() if size >= settings.minimum_detections } + storage.upsert( + "actor", + _actor_cluster_records( + retained, + { + cluster_id: state.cluster_ranges[cluster_id] + for cluster_id in retained + }, + config, + ), + batch_size=config.storage_batch_size, + cancellation=CancellationToken(), + ) return sum(retained.values()), len(retained) class ActorVisualProcessor: + def sampling(self, config: IndexConfig, info) -> FrameSampling: + return FrameSampling(frame_stride=config.frame_stride) + def batch_size(self, config: IndexConfig) -> int: return actor_config(config).batch_size def prepare( self, config: IndexConfig, + runtime: ModelRuntimePort, progress: ProgressCallback | None, ) -> ActorIndexState: - return ActorIndexState() + return ActorIndexState(models=get_actor_models(runtime)) def process( self, @@ -181,7 +281,7 @@ def process( state: ActorIndexState, info, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, ) -> None: process_actor_samples( @@ -197,7 +297,7 @@ def finalize( state: ActorIndexState, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, ) -> tuple[dict[str, Any], int]: detections, clusters = finalize_actor_index( state, diff --git a/src/vidxp/capabilities/actor/models.py b/src/vidxp/capabilities/actor/models.py new file mode 100644 index 0000000..3673fd8 --- /dev/null +++ b/src/vidxp/capabilities/actor/models.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable + +from vidxp.ports import ModelRuntimePort +from vidxp.capabilities.actor.specs import ( + SFACE_MODEL, + YUNET_MODEL, + actor_model_key, +) + + +@dataclass(frozen=True) +class ActorModels: + detector: Any + recognizer: Any + + +def get_actor_models( + runtime: ModelRuntimePort, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, +) -> ActorModels: + key = actor_model_key(runtime.device_for("actor")) + + def load() -> ActorModels: + import cv2 + + detector_path = runtime.resolve_artifact( + YUNET_MODEL, + download=download, + progress=progress, + ) + recognizer_path = runtime.resolve_artifact( + SFACE_MODEL, + download=download, + progress=progress, + ) + if progress is not None: + progress( + { + "state": "preparing", + "stage": "loading_model", + "message": "Loading OpenCV Zoo YuNet and SFace models.", + } + ) + models = ActorModels( + detector=cv2.FaceDetectorYN.create( + str(detector_path), + "", + (320, 320), + score_threshold=0.9, + nms_threshold=0.3, + top_k=5000, + ), + recognizer=cv2.FaceRecognizerSF.create(str(recognizer_path), ""), + ) + runtime.record_compute_precision( + YUNET_MODEL.capability, + YUNET_MODEL.weights_precision, + ) + runtime.record_compute_precision( + SFACE_MODEL.capability, + SFACE_MODEL.weights_precision, + ) + return models + + return runtime.get_or_load(key, load) diff --git a/src/vidxp/capabilities/actor/operations.py b/src/vidxp/capabilities/actor/operations.py index 88b7276..4846994 100644 --- a/src/vidxp/capabilities/actor/operations.py +++ b/src/vidxp/capabilities/actor/operations.py @@ -1,27 +1,42 @@ from __future__ import annotations from vidxp.capabilities.actor.results import ( + actor_cluster, actor_clusters, actor_detections, - render_actor_result, ) -from vidxp.capabilities.contracts import CapabilityContext from vidxp.capabilities.actor.schemas import ( + ActorClusterInput, + ActorClusterSummary, ActorClustersInput, ActorClustersOutput, ActorDetectionsInput, ActorDetectionsOutput, - ActorRenderInput, - ActorRenderResult, ) +from vidxp.capabilities.contracts import CapabilityContext + + +def cluster_operation( + context: CapabilityContext, + request: ActorClusterInput, +) -> ActorClusterSummary: + return actor_cluster( + context.require_config(), + request.cluster_id, + storage=context.require_storage(), + ) def clusters_operation( context: CapabilityContext, _request: ActorClustersInput, ) -> ActorClustersOutput: - return ActorClustersOutput( - clusters=actor_clusters(context.require_config()) + return actor_clusters( + context.require_config(), + storage=context.require_storage(), + page_size=_request.page_size, + cursor=_request.cursor, + media_id=_request.media_id, ) @@ -30,23 +45,10 @@ def detections_operation( request: ActorDetectionsInput, ) -> ActorDetectionsOutput: config = context.require_config() - return ActorDetectionsOutput( - cluster_id=request.cluster_id, - detections=actor_detections( - config, - request.cluster_id, - ), - ) - - -def render_operation( - context: CapabilityContext, - request: ActorRenderInput, -) -> ActorRenderResult: - config = context.require_config() - return render_actor_result( + return actor_detections( config, request.cluster_id, - request.input_path, - request.output_path, + storage=context.require_storage(), + page_size=request.page_size, + cursor=request.cursor, ) diff --git a/src/vidxp/capabilities/actor/requirements.txt b/src/vidxp/capabilities/actor/requirements.txt index 383f8ac..a737e1f 100644 --- a/src/vidxp/capabilities/actor/requirements.txt +++ b/src/vidxp/capabilities/actor/requirements.txt @@ -1,3 +1,3 @@ -face-recognition -numpy>=2.1,<3 -opencv-python +numpy>=2.3,<3 +opencv-python-headless>=5.0.0.93,<6 +pooch>=1.9,<2 diff --git a/src/vidxp/capabilities/actor/results.py b/src/vidxp/capabilities/actor/results.py index 1ab4386..fbdf17f 100644 --- a/src/vidxp/capabilities/actor/results.py +++ b/src/vidxp/capabilities/actor/results.py @@ -1,116 +1,297 @@ from __future__ import annotations -from pathlib import Path - from vidxp.capabilities.actor.schemas import ( ActorClusterSummary, + ActorClustersOutput, ActorDetection, - ActorRenderResult, + ActorDetectionsOutput, ) +from vidxp.capabilities.contracts import CapabilityRequestError from vidxp.core.contracts import IndexConfig -from vidxp.core.storage import IndexStorage -from vidxp.core.video import render_actor_video +from vidxp.core.cursors import ( + CursorError, + decode_cursor, + decode_offset_cursor, + encode_offset_cursor, +) +from vidxp.ports import IndexReader class ActorClusterNotFoundError(LookupError): """Raised when an actor cluster has no retained detections.""" -def actor_clusters( - config: IndexConfig, - *, - storage: IndexStorage | None = None, -) -> tuple[ActorClusterSummary, ...]: - if config.video_id is None: - raise ValueError("IndexConfig.video_id is required for actor results.") - owns_storage = storage is None - active_storage = storage or IndexStorage(config) +def _decode_cursor(cursor: str | None, scope: str) -> int: + if cursor is None: + return 0 try: - records = active_storage.records( - "actor", - video_id=config.video_id, + payload = decode_cursor(cursor, scope) + if set(payload) != {"version", "scope", "offset"}: + raise CursorError("The cursor payload is invalid.") + return decode_offset_cursor(cursor, scope=scope) + except CursorError as exc: + raise CapabilityRequestError("The actor cursor is invalid.") from exc + + +def _encode_cursor( + offset: int, + *, + scope: str, + has_more: bool, +) -> str | None: + return encode_offset_cursor( + offset, + scope=scope, + has_more=has_more, + ) + + +def _snapshot_scope(config: IndexConfig) -> str: + return str(config.snapshot_id or config.fingerprint()) + + +def _actor_detection(record: dict) -> ActorDetection: + return ActorDetection( + **{ + key: value + for key, value in record.items() + if not key.startswith("bbox_") and key != "video_id" + }, + media_id=str(record["video_id"]), + bbox=( + int(record["bbox_top"]), + int(record["bbox_right"]), + int(record["bbox_bottom"]), + int(record["bbox_left"]), + ), + ) + + +def _cluster_summary( + cluster_id: str, + records: list[dict], +) -> ActorClusterSummary: + if not records: + raise ActorClusterNotFoundError( + f"Actor cluster {cluster_id} was not found in the completed index." ) - finally: - if owns_storage: - active_storage.close() - - grouped: dict[str, list[dict]] = {} - for record in records: - grouped.setdefault(str(record["cluster_id"]), []).append(record) - return tuple( - ActorClusterSummary( - cluster_id=cluster_id, - video_id=config.video_id, - detection_count=len(cluster_records), - first_timestamp=min( - float(record["timestamp"]) for record in cluster_records - ), - last_timestamp=max( - float(record["timestamp"]) for record in cluster_records - ), + media_ids = {str(record["video_id"]) for record in records} + generation_ids = { + str(record["generation_id"]) + for record in records + if record.get("generation_id") is not None + } + if len(media_ids) != 1 or len(generation_ids) != 1: + raise RuntimeError( + f"Actor cluster {cluster_id!r} spans multiple index identities." ) - for cluster_id, cluster_records in sorted(grouped.items()) + timestamps = [float(record["timestamp"]) for record in records] + return ActorClusterSummary( + cluster_id=cluster_id, + media_id=next(iter(media_ids)), + generation_id=next(iter(generation_ids)), + detection_count=len(records), + first_timestamp=min(timestamps), + last_timestamp=max(timestamps), ) -def actor_detections( +def _stored_cluster_summary(record: dict) -> ActorClusterSummary: + if record.get("record_kind") != "cluster_summary": + raise RuntimeError("The actor cluster summary record is invalid.") + return ActorClusterSummary( + cluster_id=str(record["summary_cluster_id"]), + media_id=str(record["video_id"]), + generation_id=str(record["generation_id"]), + detection_count=int(record["detection_count"]), + first_timestamp=float(record["first_timestamp"]), + last_timestamp=float(record["last_timestamp"]), + ) + + +def actor_cluster( config: IndexConfig, cluster_id: str, *, - storage: IndexStorage | None = None, -) -> list[ActorDetection]: - if config.video_id is None: - raise ValueError("IndexConfig.video_id is required for actor results.") - owns_storage = storage is None - active_storage = storage or IndexStorage(config) - try: - records = active_storage.records( + storage: IndexReader, +) -> ActorClusterSummary: + del config + summaries = storage.records( + "actor", + filters={ + "record_kind": "cluster_summary", + "summary_cluster_id": cluster_id, + }, + limit=2, + ) + if len(summaries) > 1: + raise RuntimeError( + f"Actor cluster {cluster_id!r} spans multiple index identities." + ) + if summaries: + return _stored_cluster_summary(summaries[0]) + return _cluster_summary( + cluster_id, + storage.records( "actor", - video_id=config.video_id, filters={"cluster_id": cluster_id}, + ), + ) + + +def actor_clusters( + config: IndexConfig, + *, + storage: IndexReader, + page_size: int, + cursor: str | None, + media_id: str | None = None, +) -> ActorClustersOutput: + scope = ( + f"actor:clusters:{_snapshot_scope(config)}:" + f"{media_id or '*'}" + ) + offset = _decode_cursor(cursor, scope) + media_scope = {"video_id": media_id} if media_id is not None else {} + records = storage.records( + "actor", + filters={"record_kind": "cluster_summary"}, + limit=page_size + 1, + offset=offset, + **media_scope, + ) + if records: + has_more = len(records) > page_size + summaries = tuple( + _stored_cluster_summary(record) + for record in records[:page_size] ) - finally: - if owns_storage: - active_storage.close() - - detections = [ - ActorDetection( - **{ - key: value - for key, value in record.items() - if not key.startswith("bbox_") - }, - bbox=( - int(record["bbox_top"]), - int(record["bbox_right"]), - int(record["bbox_bottom"]), - int(record["bbox_left"]), + return ActorClustersOutput( + clusters=summaries, + total=None, + next_cursor=_encode_cursor( + offset + len(summaries), + scope=scope, + has_more=has_more, ), ) - for record in records - ] - return sorted( - detections, - key=lambda item: (item.frame_index, item.detection_id), + if offset: + raise CapabilityRequestError( + "The actor cursor is outside the result set." + ) + + # Indexes created before cluster summaries were materialized remain + # readable, but must use the legacy full-scan path once. + grouped: dict[str, dict] = {} + storage_offset = 0 + while True: + records = storage.records( + "actor", + limit=1000, + offset=storage_offset, + **media_scope, + ) + for record in records: + cluster_id = str(record["cluster_id"]) + timestamp = float(record["timestamp"]) + summary = grouped.setdefault( + cluster_id, + { + "media_ids": set(), + "generation_ids": set(), + "detection_count": 0, + "first_timestamp": timestamp, + "last_timestamp": timestamp, + }, + ) + summary["media_ids"].add(str(record["video_id"])) + if record.get("generation_id") is not None: + summary["generation_ids"].add(str(record["generation_id"])) + summary["detection_count"] += 1 + summary["first_timestamp"] = min( + summary["first_timestamp"], + timestamp, + ) + summary["last_timestamp"] = max( + summary["last_timestamp"], + timestamp, + ) + if len(records) < 1000: + break + storage_offset += len(records) + + summaries = [] + for cluster_id, summary in sorted(grouped.items()): + media_ids = summary["media_ids"] + generation_ids = summary["generation_ids"] + if len(media_ids) != 1 or len(generation_ids) != 1: + raise RuntimeError( + f"Actor cluster {cluster_id!r} spans multiple index identities." + ) + summaries.append( + ActorClusterSummary( + cluster_id=cluster_id, + media_id=next(iter(media_ids)), + generation_id=next(iter(generation_ids)), + detection_count=summary["detection_count"], + first_timestamp=summary["first_timestamp"], + last_timestamp=summary["last_timestamp"], + ) + ) + + total = len(summaries) + if offset > total: + raise CapabilityRequestError( + "The actor cursor is outside the result set." + ) + selected = tuple(summaries[offset : offset + page_size]) + return ActorClustersOutput( + clusters=selected, + total=total, + next_cursor=_encode_cursor( + offset + len(selected), + scope=scope, + has_more=offset + len(selected) < total, + ), ) -def render_actor_result( +def actor_detections( config: IndexConfig, cluster_id: str, - input_path: str | Path, - output_path: str | Path, *, - storage: IndexStorage | None = None, -) -> ActorRenderResult: - detections = actor_detections(config, cluster_id, storage=storage) - if not detections: + storage: IndexReader, + page_size: int, + cursor: str | None, +) -> ActorDetectionsOutput: + scope = f"actor:detections:{_snapshot_scope(config)}:{cluster_id}" + storage_offset = _decode_cursor(cursor, scope) + filters = {"cluster_id": cluster_id} + records = storage.records( + "actor", + filters=filters, + limit=page_size + 1, + offset=storage_offset, + ) + if not records and storage_offset == 0: raise ActorClusterNotFoundError( f"Actor cluster {cluster_id} was not found in the completed index." ) - destination = Path(output_path) - render_actor_video(input_path, destination, cluster_id, detections) - return ActorRenderResult( - output_path=destination, - detection_count=len(detections), + if not records: + raise CapabilityRequestError( + "The actor cursor is outside the result set." + ) + has_more = len(records) > page_size + detections = tuple( + _actor_detection(record) for record in records[:page_size] + ) + return ActorDetectionsOutput( + cluster_id=cluster_id, + detections=detections, + total=None, + next_cursor=_encode_cursor( + storage_offset + len(detections), + scope=scope, + has_more=has_more, + ), ) diff --git a/src/vidxp/capabilities/actor/schemas.py b/src/vidxp/capabilities/actor/schemas.py index 26fcea8..9867003 100644 --- a/src/vidxp/capabilities/actor/schemas.py +++ b/src/vidxp/capabilities/actor/schemas.py @@ -1,60 +1,73 @@ from __future__ import annotations -from pathlib import Path - -from pydantic import Field +from pydantic import Field, model_validator from vidxp.capabilities.contracts import CapabilityInput, CapabilityOutput +from vidxp.core.identifiers import ( + ActorClusterId, + Identifier, + IndexGenerationId, + MediaId, +) class ActorClustersInput(CapabilityInput): - pass + page_size: int = Field(default=50, gt=0, le=100) + cursor: str | None = Field(default=None, min_length=1, max_length=512) + media_id: MediaId | None = None + + +class ActorClusterInput(CapabilityInput): + cluster_id: ActorClusterId class ActorClusterSummary(CapabilityOutput): - cluster_id: str = Field(min_length=1) - video_id: str = Field(min_length=1) + cluster_id: ActorClusterId + media_id: MediaId + generation_id: IndexGenerationId detection_count: int = Field(ge=0) first_timestamp: float = Field(ge=0) last_timestamp: float = Field(ge=0) + @model_validator(mode="after") + def _validate_interval(self) -> "ActorClusterSummary": + if self.last_timestamp < self.first_timestamp: + raise ValueError("last_timestamp must not precede first_timestamp") + return self + def to_dict(self) -> dict: return self.model_dump(mode="json") class ActorClustersOutput(CapabilityOutput): clusters: tuple[ActorClusterSummary, ...] = () + total: int | None = Field(default=None, ge=0) + next_cursor: str | None = None class ActorDetectionsInput(CapabilityInput): - cluster_id: str = Field(min_length=1) + cluster_id: ActorClusterId + page_size: int = Field(default=50, gt=0, le=100) + cursor: str | None = Field(default=None, min_length=1, max_length=512) class ActorDetection(CapabilityOutput): - detection_id: str = Field(min_length=1) - cluster_id: str = Field(min_length=1) + detection_id: Identifier + cluster_id: ActorClusterId frame_index: int = Field(ge=0) timestamp: float = Field(ge=0) bbox: tuple[int, int, int, int] dataset: str split: str run_id: str - video_id: str + media_id: MediaId + generation_id: IndexGenerationId modality: str source_id: str class ActorDetectionsOutput(CapabilityOutput): - cluster_id: str = Field(min_length=1) + cluster_id: ActorClusterId detections: tuple[ActorDetection, ...] = () - - -class ActorRenderInput(CapabilityInput): - cluster_id: str = Field(min_length=1) - input_path: Path - output_path: Path - - -class ActorRenderResult(CapabilityOutput): - output_path: Path - detection_count: int = Field(gt=0) + total: int | None = Field(default=None, ge=0) + next_cursor: str | None = None diff --git a/src/vidxp/capabilities/actor/specs.py b/src/vidxp/capabilities/actor/specs.py new file mode 100644 index 0000000..e42b773 --- /dev/null +++ b/src/vidxp/capabilities/actor/specs.py @@ -0,0 +1,51 @@ +from vidxp.model_contracts import ArtifactSpec, ModelKey + + +OPENCV_ZOO_REVISION = "47534e27c9851bb1128ccc0102f1145e27f23f98" +_MODEL_ROOT = ( + "https://media.githubusercontent.com/media/opencv/opencv_zoo/" + f"{OPENCV_ZOO_REVISION}/models" +) + +YUNET_MODEL = ArtifactSpec( + capability="actor.detector", + provider="opencv-zoo", + model_id="yunet", + revision=OPENCV_ZOO_REVISION, + download_size_bytes=229_738, + url=f"{_MODEL_ROOT}/face_detection_yunet/" + "face_detection_yunet_2026may.onnx", + filename="face_detection_yunet_2026may.onnx", + sha256="ebafce4e3c118d6554634be5c27ab333b4c047a9a8c3faf1d7cf93101c22f0f0", + license="MIT", + weights_precision="float32", +) + +SFACE_MODEL = ArtifactSpec( + capability="actor.recognizer", + provider="opencv-zoo", + model_id="sface", + revision=OPENCV_ZOO_REVISION, + download_size_bytes=38_696_353, + url=f"{_MODEL_ROOT}/face_recognition_sface/" + "face_recognition_sface_2021dec.onnx", + filename="face_recognition_sface_2021dec.onnx", + sha256="0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79", + license="Apache-2.0", + weights_precision="float32", +) + + +def actor_model_key(device: str) -> ModelKey: + specs = (YUNET_MODEL, SFACE_MODEL) + providers = {spec.provider for spec in specs} + revisions = {spec.revision for spec in specs} + if len(providers) != 1 or len(revisions) != 1: + raise RuntimeError("Actor model artifacts must share provider and revision.") + return ModelKey( + capability=YUNET_MODEL.capability.split(".", 1)[0], + provider=YUNET_MODEL.provider, + model_id="+".join(spec.model_id for spec in specs), + revision=YUNET_MODEL.revision, + device=device, + ) diff --git a/src/vidxp/capabilities/contracts.py b/src/vidxp/capabilities/contracts.py index d022cde..9448077 100644 --- a/src/vidxp/capabilities/contracts.py +++ b/src/vidxp/capabilities/contracts.py @@ -1,5 +1,6 @@ from __future__ import annotations +from importlib import import_module from types import MappingProxyType from typing import Any, Callable, Mapping @@ -15,6 +16,12 @@ from vidxp.core.contracts import IndexConfig, VideoSource from vidxp.core.indexing_common import ProgressCallback +from vidxp.model_contracts import ArtifactSpec, ModelSpec +from vidxp.ports import IndexReader, ModelRuntimePort +from vidxp.application_models import CapabilityProvenance + + +CAPABILITY_CONTRACT_VERSION = 1 class _ContractModel(BaseModel): @@ -28,12 +35,19 @@ class _ContractModel(BaseModel): class CapabilityInput(BaseModel): """Base model for validated capability input.""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", allow_inf_nan=False) class CapabilityOutput(_ContractModel): """Base model for validated capability output.""" + model_config = ConfigDict( + arbitrary_types_allowed=True, + extra="forbid", + frozen=True, + allow_inf_nan=False, + ) + class CapabilityConfig(_ContractModel): """Base model for settings owned and validated by one capability.""" @@ -49,11 +63,11 @@ class RuntimeCheck(_ContractModel): def inspect(self) -> dict[str, Any]: try: detail = self.check() - except Exception as exc: + except Exception: return { "name": self.label, "ok": False, - "error": f"{type(exc).__name__}: {exc}", + "error": "runtime check failed", } result: dict[str, Any] = { "name": self.label, @@ -72,21 +86,36 @@ def applies(self, source: VideoSource | None) -> bool: ) +class RuntimeCheckBinding(_ContractModel): + """Runtime check owned by a capability or platform service.""" + + capability: str = Field(min_length=1) + check: RuntimeCheck + provenance: CapabilityProvenance | None = None + + class CapabilityContext(_ContractModel): """Runtime context shared by transport-neutral capability operations.""" config: IndexConfig | None + runtime: ModelRuntimePort + storage: IndexReader | None = None def require_config(self) -> IndexConfig: if self.config is None: raise RuntimeError("This operation requires an active index.") return self.config + def require_storage(self) -> IndexReader: + if self.storage is None: + raise RuntimeError("This operation requires an active index store.") + return self.storage + class PreparationContext(_ContractModel): """Runtime values supplied to one capability's preparation hook.""" - device: str = Field(min_length=1) + runtime: ModelRuntimePort settings: CapabilityConfig @@ -94,12 +123,12 @@ class PreparationContext(_ContractModel): class OperationDefinition(_ContractModel): - """Validated input, output, and implementation for one operation.""" + """Transport-neutral input and output metadata for one operation.""" input_model: type[BaseModel] output_model: type[BaseModel] - handler: OperationHandler requires_index: bool = True + public: bool = True @field_validator("input_model", "output_model") @classmethod @@ -111,14 +140,54 @@ def _require_model( raise ValueError("Operation schemas must be Pydantic models.") return value - def invoke( - self, - context: CapabilityContext, - payload: BaseModel | Mapping[str, Any], - ) -> BaseModel: - request = self.input_model.model_validate(payload) - result = self.handler(context, request) - return self.output_model.model_validate(result) + @model_validator(mode="after") + def _validate_public_schemas(self) -> "OperationDefinition": + if not self.public: + return self + forbidden_names = { + "path", + "input_path", + "output_path", + "storage_key", + "repository_root", + "index_directory", + } + + def inspect(value: Any) -> None: + if isinstance(value, dict): + properties = value.get("properties") + if isinstance(properties, dict): + unsafe = { + name + for name in properties + if ( + name in forbidden_names + or name.endswith("_path") + or name.endswith("_directory") + ) + } + if unsafe: + fields = ", ".join(sorted(unsafe)) + raise ValueError( + f"Public operation schema exposes {fields}." + ) + if value.get("format") in {"path", "binary"}: + raise ValueError( + "Public operation schema exposes local or binary data." + ) + if value.get("contentEncoding") == "base64": + raise ValueError( + "Public operation schema embeds binary content." + ) + for nested in value.values(): + inspect(nested) + elif isinstance(value, list): + for nested in value: + inspect(nested) + + inspect(self.input_model.model_json_schema()) + inspect(self.output_model.model_json_schema()) + return self class CapabilityIndexResult(_ContractModel): @@ -141,27 +210,22 @@ class CapabilityIndexResult(_ContractModel): [IndexConfig, tuple[VideoSource, ...]], Mapping[str, Any], ] -CLIFactory = Callable[[], Any] +ExecutorFactory = Callable[[], "CapabilityExecutor"] class CapabilityDefinition(_ContractModel): - """Everything the application needs to run one named capability.""" + """Domain metadata for one named capability.""" name: str = Field(min_length=1) description: str = Field(min_length=1) extra: str = Field(min_length=1) config_model: type[CapabilityConfig] = CapabilityConfig - runtime_checks: tuple[RuntimeCheck, ...] = () collection_name: str | None = None - indexer: IndexHandler | None = None - index_processor: Any | None = None index_stage: str | None = None + execution_group: str | None = None operations: Mapping[str, OperationDefinition] = Field(default_factory=dict) - requirement_filter: RequirementFilter | None = None - prepare: PrepareHandler | None = None - model_manifest: ModelManifest | None = None - cli_name: str | None = None - cli_factory: CLIFactory | None = None + model_specs: tuple[ModelSpec | ArtifactSpec, ...] = () + prepares_models: bool = False @field_validator("config_model") @classmethod @@ -187,29 +251,61 @@ def _freeze_operations( return MappingProxyType(dict(value)) @model_validator(mode="after") - def _require_complete_integrations(self) -> CapabilityDefinition: + def _require_complete_metadata(self) -> CapabilityDefinition: indexing_fields = ( self.collection_name, - self.indexer, self.index_stage, + self.execution_group, ) if any(value is not None for value in indexing_fields) and not all( value is not None for value in indexing_fields ): raise ValueError( "Indexable capabilities must declare collection names, " - "an indexer, and an index stage together." + "an index stage, and an execution group together." ) - if (self.cli_name is None) != (self.cli_factory is None): + if self.collection_name is None and not self.operations: raise ValueError( - "cli_name and cli_factory must either both be set or both be unset." - ) - if self.indexer is None and not self.operations: - raise ValueError( - "A capability must provide an indexer or at least one operation." + "A capability must support indexing or at least one operation." ) return self + +def module_import_check( + label: str, + module_name: str, + *attributes: str, +) -> RuntimeCheck: + def check() -> None: + module = import_module(module_name) + for attribute in attributes: + if not hasattr(module, attribute): + raise AttributeError( + f"{module_name} does not expose {attribute}." + ) + + return RuntimeCheck(label=label, check=check) + + +class CapabilityExecutor(_ContractModel): + """Infrastructure hooks bound to one capability definition.""" + + indexer: IndexHandler | None = None + index_processor: Any | None = None + operations: Mapping[str, OperationHandler] = Field(default_factory=dict) + runtime_checks: tuple[RuntimeCheck, ...] = () + requirement_filter: RequirementFilter | None = None + prepare: PrepareHandler | None = None + model_manifest: ModelManifest | None = None + + @field_validator("operations") + @classmethod + def _freeze_operation_handlers( + cls, + value: Mapping[str, OperationHandler], + ) -> Mapping[str, OperationHandler]: + return MappingProxyType(dict(value)) + def source_requirements( self, source: VideoSource, @@ -220,5 +316,38 @@ def source_requirements( return self.requirement_filter(source, requirements) +class CapabilityPlugin(_ContractModel): + definition: CapabilityDefinition + executor_factory: ExecutorFactory + contract_version: int = CAPABILITY_CONTRACT_VERSION + requirements: tuple[str, ...] = () + provenance: CapabilityProvenance | None = None + + @field_validator("requirements") + @classmethod + def _validate_requirements( + cls, + values: tuple[str, ...], + ) -> tuple[str, ...]: + for value in values: + Requirement(value) + return values + + def capability_install_hint(name: str) -> str: return f'Install the capability with: pip install "vidxp[{name}]"' + + +class CapabilityRequestError(ValueError): + """Expected invalid capability selection or options.""" + + +class CapabilityDependencyError(RuntimeError): + def __init__( + self, + capabilities: tuple[str, ...], + failures: tuple[Mapping[str, Any], ...], + ) -> None: + self.capabilities = capabilities + self.failures = failures + super().__init__("Capability dependencies are unavailable.") diff --git a/src/vidxp/capabilities/dialogue/config.py b/src/vidxp/capabilities/dialogue/config.py index 3999801..8f68833 100644 --- a/src/vidxp/capabilities/dialogue/config.py +++ b/src/vidxp/capabilities/dialogue/config.py @@ -11,12 +11,6 @@ class DialogueConfig(CapabilityConfig): embedding_batch_size: int = Field(default=128, gt=0) transcription_batch_size: int = Field(default=16, gt=0) normalize_embeddings: bool = True - sentence_model: str = Field( - default="sentence-transformers/all-MiniLM-L6-v2", - min_length=1, - ) - whisper_model: str = Field(default="large-v2", min_length=1) - alignment_language: str | None = Field(default=None, min_length=1) def dialogue_config(config: IndexConfig) -> DialogueConfig: diff --git a/src/vidxp/capabilities/dialogue/definition.py b/src/vidxp/capabilities/dialogue/definition.py index f7682e5..2d6b1c7 100644 --- a/src/vidxp/capabilities/dialogue/definition.py +++ b/src/vidxp/capabilities/dialogue/definition.py @@ -7,15 +7,17 @@ from vidxp.capabilities.contracts import ( CapabilityDefinition, + CapabilityExecutor, + CapabilityPlugin, OperationDefinition, PreparationContext, - RuntimeCheck, + module_import_check, ) -from vidxp.capabilities.dialogue.config import DialogueConfig, dialogue_config -from vidxp.capabilities.dialogue.models import ( - get_alignment_model, - get_embedder, - get_whisper_model, +from vidxp.capabilities.dialogue.config import DialogueConfig +from vidxp.capabilities.dialogue.models import get_embedder, get_whisper_model +from vidxp.capabilities.dialogue.specs import ( + FASTER_WHISPER_MODEL, + QWEN3_EMBEDDING_MODEL, ) from vidxp.capabilities.dialogue.operations import ( index_capability, @@ -24,16 +26,6 @@ from vidxp.capabilities.schemas import SearchInput, SearchResult from vidxp.core.contracts import IndexConfig, VideoSource from vidxp.core.indexing_common import ProgressCallback -from vidxp.core.video import ffmpeg_binary - - -FFMPEG = RuntimeCheck( - label="FFmpeg", - check=ffmpeg_binary, - applies_to=lambda source: source.transcript is None, -) - - def filter_requirements_for_source( source: VideoSource, requirements: tuple[Requirement, ...], @@ -52,7 +44,7 @@ def prepare_models( context: PreparationContext, progress: ProgressCallback | None, ) -> tuple[str, ...]: - settings = DialogueConfig.model_validate(context.settings) + DialogueConfig.model_validate(context.settings) prepared = [] def report(stage: str, message: str) -> None: @@ -67,36 +59,31 @@ def report(stage: str, message: str) -> None: report( "dialogue_model", - f"Preparing dialogue model: {settings.sentence_model}", + f"Preparing dialogue model: {QWEN3_EMBEDDING_MODEL.model_id}", ) - get_embedder(settings.sentence_model, context.device) - prepared.append(settings.sentence_model) + get_embedder(context.runtime, download=True, progress=progress) + prepared.append(QWEN3_EMBEDDING_MODEL.model_id) report( "transcription_model", - f"Preparing transcription model: WhisperX {settings.whisper_model}", + "Preparing transcription model: faster-whisper " + f"{FASTER_WHISPER_MODEL.model_id}", ) - get_whisper_model(settings.whisper_model, context.device) - prepared.append(settings.whisper_model) - if settings.alignment_language: - report( - "alignment_model", - f"Preparing the {settings.alignment_language} alignment model.", - ) - get_alignment_model(settings.alignment_language, context.device) - prepared.append( - f"whisperx-alignment:{settings.alignment_language}" - ) + get_whisper_model(context.runtime, download=True, progress=progress) + prepared.append(FASTER_WHISPER_MODEL.model_id) return tuple(prepared) def model_manifest( - config: IndexConfig, + _config: IndexConfig, sources: tuple[VideoSource, ...], ) -> Mapping[str, Any]: - settings = dialogue_config(config) - result: dict[str, Any] = {"dialogue": settings.sentence_model} + result: dict[str, Any] = { + "dialogue": { + **QWEN3_EMBEDDING_MODEL.identity(), + } + } if any(source.transcript is None for source in sources): - result["transcription"] = settings.whisper_model + result["transcription"] = FASTER_WHISPER_MODEL.identity() return result @@ -106,17 +93,48 @@ def model_manifest( extra="dialogue", config_model=DialogueConfig, collection_name="dialogue", - indexer=index_capability, index_stage="dialogue_indexing", - runtime_checks=(FFMPEG,), - requirement_filter=filter_requirements_for_source, - prepare=prepare_models, - model_manifest=model_manifest, + execution_group="dialogue", + prepares_models=True, + model_specs=(QWEN3_EMBEDDING_MODEL, FASTER_WHISPER_MODEL), operations={ "search": OperationDefinition( input_model=SearchInput, output_model=SearchResult, - handler=search_operation, ) }, ) + + +def create_executor() -> CapabilityExecutor: + return CapabilityExecutor( + indexer=index_capability, + operations={"search": search_operation}, + requirement_filter=filter_requirements_for_source, + prepare=prepare_models, + model_manifest=model_manifest, + runtime_checks=( + module_import_check( + "faster-whisper import", + "faster_whisper", + "WhisperModel", + "BatchedInferencePipeline", + ), + module_import_check( + "Sentence Transformers import", + "sentence_transformers", + "SentenceTransformer", + ), + module_import_check( + "Hugging Face Hub import", + "huggingface_hub", + "snapshot_download", + ), + ), + ) + + +PLUGIN = CapabilityPlugin( + definition=DEFINITION, + executor_factory=create_executor, +) diff --git a/src/vidxp/capabilities/dialogue/indexing.py b/src/vidxp/capabilities/dialogue/indexing.py index 4e5201c..f90919a 100644 --- a/src/vidxp/capabilities/dialogue/indexing.py +++ b/src/vidxp/capabilities/dialogue/indexing.py @@ -1,15 +1,14 @@ from __future__ import annotations -import hashlib from dataclasses import dataclass from pathlib import Path from typing import Any, Mapping, Sequence from vidxp.capabilities.dialogue.config import dialogue_config -from vidxp.capabilities.dialogue.models import ( - get_alignment_model, - get_embedder, - get_whisper_model, +from vidxp.capabilities.dialogue.models import get_embedder, get_whisper_model +from vidxp.capabilities.dialogue.specs import ( + FASTER_WHISPER_MODEL, + QWEN3_EMBEDDING_MODEL, ) from vidxp.core.contracts import ( CancellationToken, @@ -19,8 +18,7 @@ stable_source_id, ) from vidxp.core.indexing_common import ProgressCallback, report_progress -from vidxp.core.storage import IndexStorage -from vidxp.core.video import extract_audio +from vidxp.ports import IndexStore, ModelRuntimePort @dataclass(frozen=True) @@ -118,73 +116,63 @@ def transcribe_video( input_path: str | Path, *, config: IndexConfig, - work_directory: str | Path, cancellation: CancellationToken, + runtime: ModelRuntimePort, progress: ProgressCallback | None, -) -> tuple[list[Mapping[str, Any]], str]: - import whisperx +) -> tuple[list[Mapping[str, Any]], str | None]: + import av + from faster_whisper import BatchedInferencePipeline settings = dialogue_config(config) cancellation.raise_if_cancelled() - audio_name = hashlib.sha256( - str(config.video_id).encode("utf-8") - ).hexdigest() - audio_path = Path(work_directory) / f"{audio_name}.wav" + with av.open(str(input_path)) as container: + if not container.streams.audio: + report_progress( + progress, + "dialogue_skipped", + "No audio stream was found; dialogue indexing was skipped.", + ) + return [], None report_progress( progress, - "extracting_audio", - "Extracting audio from the video.", + "preparing_transcription_model", + "Preparing transcription model: faster-whisper " + f"{FASTER_WHISPER_MODEL.model_id}.", ) - extract_audio(input_path, audio_path) - try: - cancellation.raise_if_cancelled() - report_progress( - progress, - "preparing_transcription_model", - f"Preparing transcription model: WhisperX {settings.whisper_model}.", - ) - whisper_model = get_whisper_model( - settings.whisper_model, - config.device, - ) - audio = whisperx.load_audio(str(audio_path)) - report_progress( - progress, - "transcribing_audio", - "Transcribing the video audio.", - ) - transcription = whisper_model.transcribe( - audio, - batch_size=settings.transcription_batch_size, - ) - language = str(transcription["language"]) - + whisper_model = get_whisper_model(runtime) + report_progress( + progress, + "transcribing_audio", + "Transcribing and timestamping the video audio.", + ) + segments, info = BatchedInferencePipeline( + model=whisper_model + ).transcribe( + str(input_path), + batch_size=settings.transcription_batch_size, + word_timestamps=True, + vad_filter=True, + ) + result = [] + for segment in segments: cancellation.raise_if_cancelled() - report_progress( - progress, - "preparing_alignment_model", - f"Preparing the {language} alignment model.", - ) - alignment_model, alignment_metadata = get_alignment_model( - language, - config.device, - ) - report_progress( - progress, - "aligning_audio", - "Aligning transcript timestamps.", - ) - aligned = whisperx.align( - transcription["segments"], - alignment_model, - alignment_metadata, - audio, - config.device, - return_char_alignments=False, + result.append( + { + "text": segment.text, + "start": segment.start, + "end": segment.end, + "words": [ + { + "word": word.word, + "start": word.start, + "end": word.end, + } + for word in (segment.words or ()) + if word.start is not None and word.end is not None + ], + } ) - return list(aligned["segments"]), language - finally: - audio_path.unlink(missing_ok=True) + return result, str(info.language) def _dialogue_records( @@ -199,6 +187,7 @@ def _dialogue_records( str(config.video_id), "dialogue", f"p{phrase.phrase_id:08d}", + generation_id=config.generation_id, ) records.append( StorageRecord( @@ -221,8 +210,9 @@ def index_dialogue( source: VideoSource, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, + runtime: ModelRuntimePort, progress: ProgressCallback | None = None, ) -> dict[str, Any]: if config.video_id is None: @@ -240,8 +230,8 @@ def index_dialogue( segments, language = transcribe_video( source.path, config=config, - work_directory=config.run_directory / "work", cancellation=cancellation, + runtime=runtime, progress=progress, ) @@ -255,11 +245,11 @@ def index_dialogue( report_progress( progress, "preparing_dialogue_model", - f"Preparing dialogue model: {settings.sentence_model}.", + f"Preparing dialogue model: {QWEN3_EMBEDDING_MODEL.model_id}.", 0, len(phrases), ) - encoder = get_embedder(settings.sentence_model, config.device) + encoder = get_embedder(runtime) report_progress( progress, "dialogue_indexing", @@ -271,7 +261,7 @@ def index_dialogue( for offset in range(0, len(phrases), settings.embedding_batch_size): cancellation.raise_if_cancelled() group = phrases[offset:offset + settings.embedding_batch_size] - vectors = encoder.encode( + vectors = encoder.encode_document( [phrase.text for phrase in group], batch_size=len(group), convert_to_numpy=True, diff --git a/src/vidxp/capabilities/dialogue/models.py b/src/vidxp/capabilities/dialogue/models.py index 449d50a..7a671ea 100644 --- a/src/vidxp/capabilities/dialogue/models.py +++ b/src/vidxp/capabilities/dialogue/models.py @@ -1,30 +1,106 @@ from __future__ import annotations -from functools import lru_cache +from typing import Any, Callable +from vidxp.ports import ModelRuntimePort +from vidxp.model_contracts import loaded_compute_precision +from vidxp.capabilities.dialogue.specs import ( + FASTER_WHISPER_MODEL, + QWEN3_EMBEDDING_MODEL, + whisper_compute_type, +) -@lru_cache -def get_embedder(model_name: str, device: str): - from sentence_transformers import SentenceTransformer - return SentenceTransformer(model_name, device=device) +def get_embedder( + runtime: ModelRuntimePort, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, +) -> Any: + device = runtime.device_for("dialogue.embedding") + key = QWEN3_EMBEDDING_MODEL.key(device) + def load() -> Any: + from sentence_transformers import SentenceTransformer -@lru_cache -def get_whisper_model(model_name: str, device: str): - import whisperx + snapshot = runtime.resolve_model( + QWEN3_EMBEDDING_MODEL, + download=download, + progress=progress, + ) + if progress is not None: + progress( + { + "state": "preparing", + "stage": "loading_model", + "message": ( + f"Loading {QWEN3_EMBEDDING_MODEL.model_id}." + ), + } + ) + model = SentenceTransformer( + str(snapshot), + device=device, + cache_folder=str(runtime.model_cache), + local_files_only=True, + ) + runtime.record_compute_precision( + QWEN3_EMBEDDING_MODEL.capability, + loaded_compute_precision( + model, + fallback=QWEN3_EMBEDDING_MODEL.weights_precision, + ), + ) + return model - return whisperx.load_model(model_name, device, compute_type="float32") + return runtime.get_or_load(key, load) -@lru_cache -def get_alignment_model(language: str, device: str): - import whisperx +def get_whisper_model( + runtime: ModelRuntimePort, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, +) -> Any: + device = runtime.device_for("dialogue.transcription") + compute_type = whisper_compute_type(device) + key = FASTER_WHISPER_MODEL.key(f"{device}:{compute_type}") - return whisperx.load_align_model(language_code=language, device=device) + def load() -> Any: + from faster_whisper import WhisperModel + snapshot = runtime.resolve_model( + FASTER_WHISPER_MODEL, + download=download, + progress=progress, + ) + if progress is not None: + progress( + { + "state": "preparing", + "stage": "loading_model", + "message": ( + f"Loading {FASTER_WHISPER_MODEL.model_id}." + ), + } + ) + model = WhisperModel( + str(snapshot), + device=device.split(":", 1)[0], + device_index=( + int(device.split(":", 1)[1]) + if ":" in device + else 0 + ), + compute_type=compute_type, + cpu_threads=runtime.cpu_thread_budget, + download_root=str(runtime.model_cache), + local_files_only=True, + ) + runtime.record_compute_precision( + FASTER_WHISPER_MODEL.capability, + compute_type, + ) + return model -def clear_model_cache() -> None: - get_embedder.cache_clear() - get_whisper_model.cache_clear() - get_alignment_model.cache_clear() + return runtime.get_or_load(key, load) diff --git a/src/vidxp/capabilities/dialogue/operations.py b/src/vidxp/capabilities/dialogue/operations.py index 9fd98e3..7c6e2e9 100644 --- a/src/vidxp/capabilities/dialogue/operations.py +++ b/src/vidxp/capabilities/dialogue/operations.py @@ -6,6 +6,7 @@ CapabilityContext, CapabilityIndexResult, ) +from vidxp.capabilities.registry import CapabilityRegistry from vidxp.capabilities.dialogue.config import dialogue_config from vidxp.capabilities.dialogue.indexing import index_dialogue from vidxp.capabilities.dialogue.models import get_embedder @@ -17,7 +18,7 @@ VideoSource, ) from vidxp.core.indexing_common import ProgressCallback -from vidxp.core.storage import IndexStorage +from vidxp.ports import IndexStore, ModelRuntimePort REQUIRED_METADATA = frozenset( @@ -40,8 +41,10 @@ def index_capability( source: VideoSource, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, progress: ProgressCallback | None = None, modalities: tuple[str, ...] = ("dialogue",), ) -> CapabilityIndexResult: @@ -54,14 +57,19 @@ def index_capability( storage=storage, cancellation=cancellation, progress=progress, + runtime=runtime, ) ) -def dialogue_embedding(query: str, config: IndexConfig) -> list[float]: +def dialogue_embedding( + query: str, + config: IndexConfig, + runtime: ModelRuntimePort, +) -> list[float]: settings = dialogue_config(config) - encoder = get_embedder(settings.sentence_model, config.device) - encoded = encoder.encode( + encoder = get_embedder(runtime) + encoded = encoder.encode_query( [query], convert_to_numpy=True, normalize_embeddings=settings.normalize_embeddings, @@ -73,11 +81,12 @@ def search_dialogue( query: str, *, config: IndexConfig, + runtime: ModelRuntimePort, top_k: int = 10, video_id: str | None = None, query_id: str | None = None, filters: Mapping[str, Any] | None = None, - storage: IndexStorage | None = None, + storage: IndexStore, ) -> SearchResult: cleaned = query.strip() if not cleaned: @@ -87,7 +96,7 @@ def search_dialogue( return search_embeddings( cleaned, "dialogue", - dialogue_embedding(cleaned, config), + dialogue_embedding(cleaned, config, runtime), config=config, required_metadata=REQUIRED_METADATA, top_k=top_k, @@ -107,5 +116,7 @@ def search_operation( request.query, config=config, top_k=request.top_k, - video_id=config.video_id, + video_id=request.media_id or config.video_id, + runtime=context.runtime, + storage=context.require_storage(), ) diff --git a/src/vidxp/capabilities/dialogue/requirements.txt b/src/vidxp/capabilities/dialogue/requirements.txt index c97c911..0841436 100644 --- a/src/vidxp/capabilities/dialogue/requirements.txt +++ b/src/vidxp/capabilities/dialogue/requirements.txt @@ -1,3 +1,3 @@ -moviepy==1.0.3 -sentence-transformers>=3.4,<4 -whisperx>=3.8.6,<3.9 +faster-whisper>=1.2.1,<2 +sentence-transformers>=5.6.1,<6 +huggingface-hub>=1.25.1,<2 diff --git a/src/vidxp/capabilities/dialogue/specs.py b/src/vidxp/capabilities/dialogue/specs.py new file mode 100644 index 0000000..ac889c8 --- /dev/null +++ b/src/vidxp/capabilities/dialogue/specs.py @@ -0,0 +1,34 @@ +from vidxp.model_contracts import ModelSpec + + +QWEN3_EMBEDDING_MODEL = ModelSpec( + capability="dialogue.embedding", + provider="sentence-transformers", + model_id="Qwen/Qwen3-Embedding-0.6B", + revision="97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3", + download_size_bytes=1_207_489_041, + weights_file="model.safetensors", + weights_sha256=( + "0437e45c94563b09e13cb7a64478fc406947a93cb34a7e05870fc8dcd48e23fd" + ), + license="Apache-2.0", + weights_precision="bfloat16", +) + +FASTER_WHISPER_MODEL = ModelSpec( + capability="dialogue.transcription", + provider="faster-whisper", + model_id="dropbox-dash/faster-whisper-large-v3-turbo", + revision="0a363e9161cbc7ed1431c9597a8ceaf0c4f78fcf", + download_size_bytes=1_621_668_947, + weights_file="model.bin", + weights_sha256=( + "e76620f83d5f5b69efd3d87e3dc180c1bd21df9fbebacfd4335e5e1efcc018da" + ), + license="MIT", + weights_precision="float16", +) + + +def whisper_compute_type(device: str) -> str: + return "float16" if device.startswith("cuda") else "int8" diff --git a/src/vidxp/capabilities/registry.py b/src/vidxp/capabilities/registry.py index 2a7a633..b18d9e9 100644 --- a/src/vidxp/capabilities/registry.py +++ b/src/vidxp/capabilities/registry.py @@ -1,19 +1,32 @@ from __future__ import annotations +from dataclasses import dataclass +from importlib.metadata import EntryPoint, entry_points +from pathlib import Path +from threading import RLock +from time import perf_counter from types import MappingProxyType -from typing import Any, Iterable, Mapping +from typing import Any, Callable, Iterable, Mapping from packaging.requirements import Requirement from packaging.utils import canonicalize_name -from vidxp.capabilities.actor.definition import DEFINITION as ACTOR from vidxp.capabilities.contracts import ( + CAPABILITY_CONTRACT_VERSION, CapabilityDefinition, + CapabilityDependencyError, + CapabilityExecutor, + CapabilityPlugin, + CapabilityProvenance, + CapabilityRequestError, RuntimeCheck, + RuntimeCheckBinding, capability_install_hint, ) -from vidxp.capabilities.dialogue.definition import DEFINITION as DIALOGUE -from vidxp.capabilities.scene.definition import DEFINITION as SCENE +from vidxp.application_models import ( + CapabilityDependencyCheck, + DependencyKind, +) from vidxp.core.contracts import VideoSource from vidxp.dependencies import ( active_requirements, @@ -21,189 +34,542 @@ installed_base_requirements, packaged_requirements, ) +from vidxp.model_contracts import ArtifactSpec, ModelSpec +from vidxp.model_contracts import model_artifact_cached -_BUILT_INS = (DIALOGUE, SCENE, ACTOR) -CAPABILITIES = MappingProxyType( - {capability.name: capability for capability in _BUILT_INS} -) +ENTRY_POINT_GROUP = "vidxp.capabilities" -if len(CAPABILITIES) != len(_BUILT_INS): - raise RuntimeError("Capability names must be unique.") +@dataclass(frozen=True) +class _RequirementBinding: + capability: str + provenance: CapabilityProvenance | None + requirement: Requirement -def capability_names() -> tuple[str, ...]: - return tuple(CAPABILITIES) +class CapabilityRegistry: + """Validated capability metadata with lazily constructed executors.""" -def index_capability_names() -> tuple[str, ...]: - return tuple( - name - for name, capability in CAPABILITIES.items() - if capability.indexer is not None - ) + def __init__( + self, + plugins: Iterable[CapabilityPlugin], + *, + platform_runtime_checks: Iterable[RuntimeCheckBinding] = (), + storage_requirements: Iterable[Requirement] | None = None, + ) -> None: + indexed: dict[str, CapabilityPlugin] = {} + for plugin in plugins: + if plugin.contract_version != CAPABILITY_CONTRACT_VERSION: + raise RuntimeError( + f"Capability {plugin.definition.name!r} from " + f"{self._label(plugin)} targets contract " + f"{plugin.contract_version}; VidXP requires " + f"{CAPABILITY_CONTRACT_VERSION}." + ) + name = plugin.definition.name + if name in indexed: + existing = self._label(indexed[name]) + incoming = self._label(plugin) + raise RuntimeError( + f"Duplicate capability name {name!r}: " + f"{existing} conflicts with {incoming}." + ) + indexed[name] = plugin + self._plugins = MappingProxyType(indexed) + self._platform_runtime_checks = tuple(platform_runtime_checks) + self._storage_requirements = tuple( + storage_requirements + if storage_requirements is not None + else active_requirements( + packaged_requirements( + "vidxp", + "requirements/storage.txt", + ) + ) + ) + self._executors: dict[str, CapabilityExecutor] = {} + self._executor_lock = RLock() + @staticmethod + def _label(plugin: CapabilityPlugin) -> str: + if plugin.provenance is None: + return "built-in VidXP capability" + return ( + f"{plugin.provenance.distribution}:" + f"{plugin.provenance.entry_point}" + ) -def preparable_capability_names() -> tuple[str, ...]: - return tuple( - name - for name, capability in CAPABILITIES.items() - if capability.prepare is not None - ) + @property + def definitions(self) -> Mapping[str, CapabilityDefinition]: + return MappingProxyType( + {name: plugin.definition for name, plugin in self._plugins.items()} + ) + def names(self) -> tuple[str, ...]: + return tuple(self._plugins) -def get_capability(name: str) -> CapabilityDefinition: - try: - return CAPABILITIES[name] - except KeyError as exc: - available = ", ".join(capability_names()) - raise ValueError( - f"Unknown capability {name!r}. Available capabilities: {available}." - ) from exc - - -def validate_capability_names(names: Iterable[str]) -> tuple[str, ...]: - selected = tuple(dict.fromkeys(str(name).strip() for name in names)) - if not selected: - raise ValueError("At least one capability is required.") - for name in selected: - get_capability(name) - return selected - - -def collection_names( - names: Iterable[str] | None = None, -) -> dict[str, str]: - selected = ( - index_capability_names() - if names is None - else validate_capability_names(names) - ) - return { - name: get_capability(name).collection_name - for name in selected - if get_capability(name).indexer is not None - } + def provenance(self, name: str) -> CapabilityProvenance | None: + self.get(name) + return self._plugins[name].provenance + def index_names(self) -> tuple[str, ...]: + return tuple( + name + for name, definition in self.definitions.items() + if definition.collection_name is not None + ) -def validate_capability_options( - names: Iterable[str], - options: Mapping[str, Mapping[str, Any]] | None, -) -> dict[str, dict[str, Any]]: - selected = validate_capability_names(names) - supplied = dict(options or {}) - unknown = sorted(set(supplied) - set(selected)) - if unknown: - raise ValueError( - "Options were supplied for disabled capabilities: " - + ", ".join(unknown) + def preparable_names(self) -> tuple[str, ...]: + return tuple( + name + for name, definition in self.definitions.items() + if definition.prepares_models ) - return { - name: get_capability(name) - .config_model.model_validate(supplied.get(name, {})) - .model_dump(mode="python") - for name in selected - } + def model_specs( + self, + names: Iterable[str] | None = None, + ) -> tuple[ModelSpec | ArtifactSpec, ...]: + selected = self.names() if names is None else self.validate_names(names) + return tuple( + dict.fromkeys( + spec + for name in selected + for spec in self.get(name).model_specs + ) + ) -def requirements_for( - names: Iterable[str], - *, - source: VideoSource | None = None, -) -> tuple[Requirement, ...]: - selected = tuple( - get_capability(name) - for name in validate_capability_names(names) - ) - requirements = list( - active_requirements( - packaged_requirements( - "vidxp", - "requirements/storage.txt", + def model_checks( + self, + names: Iterable[str], + *, + cache: Path, + on_check_start: ( + Callable[[str, DependencyKind, str], None] | None + ) = None, + on_check_complete: ( + Callable[[CapabilityDependencyCheck, float], None] | None + ) = None, + ) -> tuple[CapabilityDependencyCheck, ...]: + selected = self.validate_names(names) + checks = [] + for name in selected: + for spec in self.get(name).model_specs: + if on_check_start is not None: + on_check_start(name, DependencyKind.model, spec.model_id) + started = perf_counter() + cached = model_artifact_cached(cache, spec) + check = CapabilityDependencyCheck( + capability=name, + kind=DependencyKind.model, + name=spec.model_id, + download_size_bytes=spec.download_size_bytes, + ok=cached, + error=( + None + if cached + else ( + "model artifacts are not prepared; run " + f"vidxp prepare --modalities {name}" + ) + ), + ) + checks.append(check) + if on_check_complete is not None: + on_check_complete(check, perf_counter() - started) + return tuple(checks) + + def get(self, name: str) -> CapabilityDefinition: + try: + return self._plugins[name].definition + except KeyError as exc: + available = ", ".join(self.names()) + raise CapabilityRequestError( + f"Unknown capability {name!r}. " + f"Available capabilities: {available}." + ) from exc + + def executor(self, name: str) -> CapabilityExecutor: + definition = self.get(name) + with self._executor_lock: + if name not in self._executors: + executor = self._plugins[name].executor_factory() + operation_names = set(definition.operations) + if set(executor.operations) != operation_names: + raise RuntimeError( + f"Capability {name!r} from " + f"{self._label(self._plugins[name])} has operation " + "handlers that do not match its declaration." + ) + if (executor.indexer is not None) != ( + definition.collection_name is not None + ): + raise RuntimeError( + f"Capability {name!r} from " + f"{self._label(self._plugins[name])} has indexing " + "metadata that does not match its executor." + ) + if (executor.prepare is not None) != definition.prepares_models: + raise RuntimeError( + f"Capability {name!r} from " + f"{self._label(self._plugins[name])} has preparation " + "metadata that does not match its executor." + ) + self._executors[name] = executor + return self._executors[name] + + def validate_names(self, names: Iterable[str]) -> tuple[str, ...]: + selected = tuple(dict.fromkeys(str(name).strip() for name in names)) + if not selected: + raise CapabilityRequestError("At least one capability is required.") + for name in selected: + self.get(name) + return selected + + def collection_names( + self, + names: Iterable[str] | None = None, + ) -> dict[str, str]: + selected = self.index_names() if names is None else self.validate_names(names) + return { + name: collection_name + for name in selected + if (collection_name := self.get(name).collection_name) is not None + } + + def validate_options( + self, + names: Iterable[str], + options: Mapping[str, Mapping[str, Any]] | None, + ) -> dict[str, dict[str, Any]]: + selected = self.validate_names(names) + supplied = dict(options or {}) + unknown = sorted(set(supplied) - set(selected)) + if unknown: + raise CapabilityRequestError( + "Options were supplied for disabled capabilities: " + + ", ".join(unknown) + ) + return { + name: self.get(name) + .config_model.model_validate(supplied.get(name, {})) + .model_dump(mode="python") + for name in selected + } + + def requirements_for( + self, + names: Iterable[str], + *, + source: VideoSource | None = None, + ) -> tuple[Requirement, ...]: + bindings = self._requirement_bindings(names, source=source) + unique = { + str(binding.requirement): binding.requirement for binding in bindings + } + return tuple(unique.values()) + + def _requirement_bindings( + self, + names: Iterable[str], + *, + source: VideoSource | None = None, + ) -> tuple[_RequirementBinding, ...]: + selected = self.validate_names(names) + bindings = [ + _RequirementBinding("storage", None, requirement) + for requirement in self._storage_requirements + if any(self.get(name).collection_name for name in selected) + ] + for name in selected: + plugin = self._plugins[name] + capability_requirements = active_requirements( + tuple(Requirement(value) for value in plugin.requirements) + if plugin.provenance is not None + else packaged_requirements(f"vidxp.capabilities.{name}") + ) + executor = self.executor(name) + selected_requirements = ( + capability_requirements + if source is None + else executor.source_requirements(source, capability_requirements) + ) + bindings.extend( + _RequirementBinding(name, plugin.provenance, requirement) + for requirement in selected_requirements + ) + unique = { + ( + binding.capability, + str(binding.requirement), + ): binding + for binding in bindings + } + return tuple(unique.values()) + + def install_hint(self, names: Iterable[str]) -> str: + selected = self.validate_names(names) + external = [ + plugin.provenance.distribution + for name in selected + if (plugin := self._plugins[name]).provenance is not None + ] + builtins = [ + self.get(name).extra + for name in selected + if self._plugins[name].provenance is None + ] + commands = [] + if builtins: + commands.append( + capability_install_hint(",".join(dict.fromkeys(builtins))) + ) + if external: + commands.append( + "Install external capability distributions with: pip install " + + " ".join(dict.fromkeys(external)) + ) + return " ".join(commands) + + def runtime_checks_for( + self, + names: Iterable[str], + *, + source: VideoSource | None = None, + ) -> tuple[RuntimeCheck, ...]: + return tuple( + binding.check + for binding in self._runtime_check_bindings( + names, + source=source, ) ) - if any(capability.indexer is not None for capability in selected) - else () - ) - for capability in selected: - capability_requirements = active_requirements( - packaged_requirements( - f"vidxp.capabilities.{capability.name}" + + def _runtime_check_bindings( + self, + names: Iterable[str], + *, + source: VideoSource | None = None, + ) -> tuple[RuntimeCheckBinding, ...]: + selected = self.validate_names(names) + bindings = [ + binding + for binding in self._platform_runtime_checks + if any(self.get(name).collection_name for name in selected) + and binding.check.applies(source) + ] + for name in selected: + plugin = self._plugins[name] + bindings.extend( + RuntimeCheckBinding( + capability=name, + provenance=plugin.provenance, + check=check, + ) + for check in self.executor(name).runtime_checks + if check.applies(source) + ) + unique = { + ( + binding.capability, + binding.check.label, + ( + binding.provenance.distribution, + binding.provenance.entry_point, + ) + if binding.provenance is not None + else None, + ): binding + for binding in bindings + } + return tuple(unique.values()) + + def dependency_checks( + self, + names: Iterable[str], + *, + source: VideoSource | None = None, + include_runtime_checks: bool = True, + on_check_start: ( + Callable[[str, DependencyKind, str], None] | None + ) = None, + on_check_complete: ( + Callable[[CapabilityDependencyCheck, float], None] | None + ) = None, + ) -> tuple[CapabilityDependencyCheck, ...]: + selected = self.validate_names(names) + checks = [] + for binding in self._requirement_bindings(selected, source=source): + if on_check_start is not None: + on_check_start( + binding.capability, + DependencyKind.distribution, + binding.requirement.name, + ) + started = perf_counter() + result = inspect_requirement(binding.requirement) + check = CapabilityDependencyCheck( + capability=binding.capability, + provenance=binding.provenance, + kind=DependencyKind.distribution, + **result, ) + checks.append(check) + if on_check_complete is not None: + on_check_complete(check, perf_counter() - started) + if include_runtime_checks: + for binding in self._runtime_check_bindings( + selected, + source=source, + ): + if on_check_start is not None: + on_check_start( + binding.capability, + DependencyKind.runtime, + binding.check.label, + ) + started = perf_counter() + result = binding.check.inspect() + check = CapabilityDependencyCheck( + capability=binding.capability, + provenance=binding.provenance, + kind=DependencyKind.runtime, + name=result["name"], + ok=result["ok"], + error=result["error"], + ) + checks.append(check) + if on_check_complete is not None: + on_check_complete( + check, + perf_counter() - started, + ) + return tuple(checks) + + def require_dependencies( + self, + names: Iterable[str], + *, + source: VideoSource, + ) -> None: + selected = self.validate_names(names) + failures = tuple( + check + for check in self.dependency_checks(selected, source=source) + if not check.ok ) - requirements.extend( - capability_requirements - if source is None - else capability.source_requirements( - source, - capability_requirements, + if failures: + raise CapabilityDependencyError( + selected, + tuple( + check.model_dump(mode="json") + for check in failures + ), ) + + def runtime_distributions(self) -> tuple[str, ...]: + distributions = { + canonicalize_name(requirement.name) + for requirement in installed_base_requirements() + } + distributions.update( + canonicalize_name(requirement.name) + for requirement in self.requirements_for(self.names()) ) - unique = {str(requirement): requirement for requirement in requirements} - return tuple(unique.values()) + return tuple(sorted(distributions, key=str.lower)) -def runtime_checks_for( - names: Iterable[str], - *, - source: VideoSource | None = None, -) -> tuple[RuntimeCheck, ...]: - checks = ( - check - for name in validate_capability_names(names) - for check in get_capability(name).runtime_checks - if check.applies(source) - ) - return tuple({check.label: check for check in checks}.values()) +def _builtin_plugins() -> tuple[CapabilityPlugin, ...]: + from vidxp.capabilities.actor.definition import PLUGIN as actor + from vidxp.capabilities.dialogue.definition import PLUGIN as dialogue + from vidxp.capabilities.scene.definition import PLUGIN as scene + + return dialogue, scene, actor -def dependency_checks(names: Iterable[str]) -> tuple[dict, ...]: +def _external_entry_points(allowlist: tuple[str, ...]) -> tuple[EntryPoint, ...]: + allowed = { + tuple(canonicalize_name(part) for part in value.split(":", 1)) + for value in allowlist + } + candidates = entry_points(group=ENTRY_POINT_GROUP) return tuple( - inspect_requirement(requirement) - for requirement in requirements_for(names) - ) + tuple( - check.inspect() - for check in runtime_checks_for(names) + sorted( + ( + entry_point + for entry_point in candidates + if entry_point.dist is not None + and ( + canonicalize_name(entry_point.dist.name), + canonicalize_name(entry_point.name), + ) + in allowed + ), + key=lambda item: ( + canonicalize_name( + item.dist.name if item.dist is not None else "" + ), + canonicalize_name(item.name), + ), + ) ) -def require_dependencies( - names: Iterable[str], +def create_capability_registry( *, - source: VideoSource, -) -> None: - selected = validate_capability_names(names) - failures = [ - result - for requirement in requirements_for(selected, source=source) - if not (result := inspect_requirement(requirement))["ok"] - ] - failures.extend( - result - for check in runtime_checks_for(selected, source=source) - if not (result := check.inspect())["ok"] - ) - if failures: - details = "; ".join( - f"{failure['name']}: {failure['error']}" - for failure in failures - ) - extras = ",".join( - get_capability(name).extra for name in selected - ) - raise RuntimeError( - f"Capability dependencies are unavailable: {details}. " - + capability_install_hint(extras) - ) - - -def runtime_distributions() -> tuple[str, ...]: - distributions = { - canonicalize_name(requirement.name) - for requirement in installed_base_requirements() - } - distributions.update( - canonicalize_name(requirement.name) - for requirement in requirements_for(capability_names()) + external: bool = False, + allowlist: tuple[str, ...] = (), + platform_runtime_checks: tuple[RuntimeCheckBinding, ...] = (), + storage_requirements: Iterable[Requirement] | None = None, +) -> CapabilityRegistry: + plugins = list(_builtin_plugins()) + if external: + for entry_point in _external_entry_points(allowlist): + distribution = entry_point.dist + identity = ( + f"{distribution.name}:{entry_point.name}" + if distribution is not None + else entry_point.name + ) + try: + loaded = entry_point.load() + plugin = ( + loaded() + if callable(loaded) + and not isinstance(loaded, CapabilityPlugin) + else loaded + ) + except Exception as exc: + raise RuntimeError( + f"Could not load capability entry point " + f"{identity!r}." + ) from exc + if not isinstance(plugin, CapabilityPlugin): + raise RuntimeError( + f"Capability entry point {identity!r} did not " + "return a CapabilityPlugin." + ) + plugin = plugin.model_copy( + update={ + "provenance": CapabilityProvenance( + distribution=( + distribution.name + if distribution is not None + else "unknown" + ), + entry_point=entry_point.name, + version=( + distribution.version + if distribution is not None + else None + ), + ) + } + ) + plugins.append(plugin) + return CapabilityRegistry( + plugins, + platform_runtime_checks=platform_runtime_checks, + storage_requirements=storage_requirements, ) - return tuple(sorted(distributions, key=str.lower)) diff --git a/src/vidxp/capabilities/scene/config.py b/src/vidxp/capabilities/scene/config.py index 87b2e34..d52501b 100644 --- a/src/vidxp/capabilities/scene/config.py +++ b/src/vidxp/capabilities/scene/config.py @@ -8,7 +8,7 @@ class SceneConfig(CapabilityConfig): batch_size: int = Field(default=32, gt=0) - model: str = Field(default="ViT-B/32", min_length=1) + sample_fps: float = Field(default=1.0, gt=0) def scene_config(config: IndexConfig) -> SceneConfig: diff --git a/src/vidxp/capabilities/scene/definition.py b/src/vidxp/capabilities/scene/definition.py index 6deaf99..aee444c 100644 --- a/src/vidxp/capabilities/scene/definition.py +++ b/src/vidxp/capabilities/scene/definition.py @@ -4,12 +4,16 @@ from vidxp.capabilities.contracts import ( CapabilityDefinition, + CapabilityExecutor, + CapabilityPlugin, OperationDefinition, PreparationContext, + module_import_check, ) -from vidxp.capabilities.scene.config import SceneConfig, scene_config +from vidxp.capabilities.scene.config import SceneConfig from vidxp.capabilities.scene.indexing import VISUAL_PROCESSOR -from vidxp.capabilities.scene.models import get_clip_model +from vidxp.capabilities.scene.models import get_scene_model +from vidxp.capabilities.scene.specs import SIGLIP2_MODEL from vidxp.capabilities.scene.operations import search_operation from vidxp.capabilities.schemas import SearchInput, SearchResult from vidxp.capabilities.visual import index_capabilities @@ -20,26 +24,30 @@ def prepare_models( context: PreparationContext, progress: ProgressCallback | None, ) -> tuple[str, ...]: - settings = SceneConfig.model_validate(context.settings) + SceneConfig.model_validate(context.settings) if progress is not None: progress( { "state": "preparing", "stage": "scene_model", "message": ( - f"Preparing scene model: CLIP {settings.model}" + f"Preparing scene model: SigLIP2 {SIGLIP2_MODEL.model_id}" ), } ) - get_clip_model(settings.model, context.device) - return (settings.model,) + get_scene_model(context.runtime, download=True, progress=progress) + return (SIGLIP2_MODEL.model_id,) def model_manifest( config: IndexConfig, _sources: tuple[VideoSource, ...], ) -> Mapping[str, Any]: - return {"scene": scene_config(config).model} + return { + "scene": { + **SIGLIP2_MODEL.identity(), + } + } DEFINITION = CapabilityDefinition( @@ -48,16 +56,45 @@ def model_manifest( extra="scene", config_model=SceneConfig, collection_name="scene", - indexer=index_capabilities, - index_processor=VISUAL_PROCESSOR, index_stage="visual_indexing", - prepare=prepare_models, - model_manifest=model_manifest, + execution_group="visual", + prepares_models=True, + model_specs=(SIGLIP2_MODEL,), operations={ "search": OperationDefinition( input_model=SearchInput, output_model=SearchResult, - handler=search_operation, ) }, ) + + +def create_executor() -> CapabilityExecutor: + return CapabilityExecutor( + indexer=index_capabilities, + index_processor=VISUAL_PROCESSOR, + operations={"search": search_operation}, + prepare=prepare_models, + model_manifest=model_manifest, + runtime_checks=( + module_import_check("OpenCV import", "cv2", "VideoCapture"), + module_import_check("Torch import", "torch"), + module_import_check( + "Transformers import", + "transformers", + "AutoModel", + "AutoProcessor", + ), + module_import_check( + "Hugging Face Hub import", + "huggingface_hub", + "snapshot_download", + ), + ), + ) + + +PLUGIN = CapabilityPlugin( + definition=DEFINITION, + executor_factory=create_executor, +) diff --git a/src/vidxp/capabilities/scene/indexing.py b/src/vidxp/capabilities/scene/indexing.py index 33cb1a0..5f721e6 100644 --- a/src/vidxp/capabilities/scene/indexing.py +++ b/src/vidxp/capabilities/scene/indexing.py @@ -4,7 +4,8 @@ from typing import Any from vidxp.capabilities.scene.config import scene_config -from vidxp.capabilities.scene.models import get_clip_model +from vidxp.capabilities.scene.models import SceneModel, get_scene_model +from vidxp.capabilities.scene.specs import SIGLIP2_MODEL from vidxp.core.contracts import ( CancellationToken, IndexConfig, @@ -13,26 +14,35 @@ stable_source_id, ) from vidxp.core.indexing_common import ProgressCallback, report_progress -from vidxp.core.storage import IndexStorage +from vidxp.core.video import FrameSampling +from vidxp.ports import IndexStore, ModelRuntimePort @dataclass class SceneIndexState: - model: Any - preprocess: Any + provider: SceneModel stored_frames: int = 0 -def encode_scene_batch(samples, model, preprocess, device): +def scene_sampling(config: IndexConfig, info) -> FrameSampling: + return FrameSampling( + source_fps=info.fps, + target_fps=scene_config(config).sample_fps, + ) + + +def encode_scene_batch(samples, provider: SceneModel): import torch from PIL import Image - images = torch.stack( - [preprocess(Image.fromarray(sample.frame)) for sample in samples] - ).to(device) - with torch.no_grad(): - features = model.encode_image(images) - features /= features.norm(dim=-1, keepdim=True) + inputs = provider.processor( + images=[Image.fromarray(sample.frame) for sample in samples], + return_tensors="pt", + ) + inputs = {name: value.to(provider.device) for name, value in inputs.items()} + with torch.inference_mode(): + features = provider.model.get_image_features(**inputs).pooler_output + features = torch.nn.functional.normalize(features, dim=-1) return features.cpu().numpy().tolist() @@ -43,10 +53,12 @@ def scene_records( config: IndexConfig, ) -> list[StorageRecord]: records = [] + sampling = scene_sampling(config, info) for sample, vector in zip(samples, vectors): - end = min( - info.duration, - sample.timestamp + config.frame_stride / info.fps, + end = sampling.next_sample_timestamp( + sample.frame_index, + frame_count=info.frame_count, + duration=info.duration, ) if end <= sample.timestamp: end = sample.timestamp + 1 / info.fps @@ -55,6 +67,7 @@ def scene_records( str(config.video_id), "scene", f"f{sample.frame_index:012d}", + generation_id=config.generation_id, ) records.append( StorageRecord( @@ -80,7 +93,7 @@ def process_scene_samples( state: SceneIndexState, info, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, ) -> None: settings = scene_config(config) @@ -88,9 +101,7 @@ def process_scene_samples( cancellation.raise_if_cancelled() vectors = encode_scene_batch( group, - state.model, - state.preprocess, - config.device, + state.provider, ) state.stored_frames += storage.upsert( "scene", @@ -101,22 +112,25 @@ def process_scene_samples( class SceneVisualProcessor: + def sampling(self, config: IndexConfig, info) -> FrameSampling: + return scene_sampling(config, info) + def batch_size(self, config: IndexConfig) -> int: return scene_config(config).batch_size def prepare( self, config: IndexConfig, + runtime: ModelRuntimePort, progress: ProgressCallback | None, ) -> SceneIndexState: - settings = scene_config(config) report_progress( progress, "preparing_scene_model", - f"Preparing scene model: CLIP {settings.model}.", + f"Preparing scene model: SigLIP2 {SIGLIP2_MODEL.model_id}.", ) - model, preprocess = get_clip_model(settings.model, config.device) - return SceneIndexState(model, preprocess) + provider = get_scene_model(runtime) + return SceneIndexState(provider) def process( self, @@ -125,7 +139,7 @@ def process( state: SceneIndexState, info, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, ) -> None: process_scene_samples( @@ -142,7 +156,7 @@ def finalize( state: SceneIndexState, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, ) -> tuple[dict[str, Any], int]: return {"scene_frames": state.stored_frames}, state.stored_frames diff --git a/src/vidxp/capabilities/scene/models.py b/src/vidxp/capabilities/scene/models.py index 380f9f1..50d8da8 100644 --- a/src/vidxp/capabilities/scene/models.py +++ b/src/vidxp/capabilities/scene/models.py @@ -1,14 +1,62 @@ from __future__ import annotations -from functools import lru_cache +from dataclasses import dataclass +from typing import Any, Callable +from vidxp.ports import ModelRuntimePort +from vidxp.model_contracts import loaded_compute_precision +from vidxp.capabilities.scene.specs import SIGLIP2_MODEL -@lru_cache -def get_clip_model(model_name: str, device: str): - import clip - return clip.load(model_name, device=device) +@dataclass(frozen=True) +class SceneModel: + model: Any + processor: Any + device: str -def clear_model_cache() -> None: - get_clip_model.cache_clear() +def get_scene_model( + runtime: ModelRuntimePort, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, +) -> SceneModel: + device = runtime.device_for("scene") + key = SIGLIP2_MODEL.key(device) + + def load() -> SceneModel: + from transformers import AutoModel, AutoProcessor + + snapshot = runtime.resolve_model( + SIGLIP2_MODEL, + download=download, + progress=progress, + ) + if progress is not None: + progress( + { + "state": "preparing", + "stage": "loading_model", + "message": f"Loading {SIGLIP2_MODEL.model_id}.", + } + ) + common = { + "cache_dir": str(runtime.model_cache), + "local_files_only": True, + } + model = AutoModel.from_pretrained(snapshot, **common).to(device) + model.eval() + runtime.record_compute_precision( + SIGLIP2_MODEL.capability, + loaded_compute_precision( + model, + fallback=SIGLIP2_MODEL.weights_precision, + ), + ) + return SceneModel( + model=model, + processor=AutoProcessor.from_pretrained(snapshot, **common), + device=device, + ) + + return runtime.get_or_load(key, load) diff --git a/src/vidxp/capabilities/scene/operations.py b/src/vidxp/capabilities/scene/operations.py index be04be7..29b1a9b 100644 --- a/src/vidxp/capabilities/scene/operations.py +++ b/src/vidxp/capabilities/scene/operations.py @@ -3,12 +3,11 @@ from typing import Any, Mapping from vidxp.capabilities.contracts import CapabilityContext -from vidxp.capabilities.scene.config import scene_config -from vidxp.capabilities.scene.models import get_clip_model +from vidxp.capabilities.scene.models import get_scene_model from vidxp.capabilities.schemas import SearchInput, SearchResult from vidxp.capabilities.search import search_embeddings from vidxp.core.contracts import IndexConfig -from vidxp.core.storage import IndexStorage +from vidxp.ports import IndexStore, ModelRuntimePort REQUIRED_METADATA = frozenset( @@ -29,16 +28,22 @@ ) -def scene_embedding(query: str, config: IndexConfig) -> list[float]: - import clip +def scene_embedding( + query: str, + runtime: ModelRuntimePort, +) -> list[float]: import torch - settings = scene_config(config) - model, _ = get_clip_model(settings.model, config.device) - tokens = clip.tokenize([query]).to(config.device) - with torch.no_grad(): - features = model.encode_text(tokens) - features /= features.norm(dim=-1, keepdim=True) + provider = get_scene_model(runtime) + inputs = provider.processor( + text=[query], + padding="max_length", + return_tensors="pt", + ) + inputs = {name: value.to(provider.device) for name, value in inputs.items()} + with torch.inference_mode(): + features = provider.model.get_text_features(**inputs).pooler_output + features = torch.nn.functional.normalize(features, dim=-1) return features.cpu().numpy().tolist()[0] @@ -46,11 +51,12 @@ def search_scene( query: str, *, config: IndexConfig, + runtime: ModelRuntimePort, top_k: int = 10, video_id: str | None = None, query_id: str | None = None, filters: Mapping[str, Any] | None = None, - storage: IndexStorage | None = None, + storage: IndexStore, ) -> SearchResult: cleaned = query.strip() if not cleaned: @@ -60,7 +66,7 @@ def search_scene( return search_embeddings( cleaned, "scene", - scene_embedding(cleaned, config), + scene_embedding(cleaned, runtime), config=config, required_metadata=REQUIRED_METADATA, top_k=top_k, @@ -80,5 +86,7 @@ def search_operation( request.query, config=config, top_k=request.top_k, - video_id=config.video_id, + video_id=request.media_id or config.video_id, + runtime=context.runtime, + storage=context.require_storage(), ) diff --git a/src/vidxp/capabilities/scene/requirements.txt b/src/vidxp/capabilities/scene/requirements.txt index 213eddc..ac5dfd9 100644 --- a/src/vidxp/capabilities/scene/requirements.txt +++ b/src/vidxp/capabilities/scene/requirements.txt @@ -1,7 +1,6 @@ -clip-anytorch==2.6.0 -numpy>=2.1,<3 -opencv-python -Pillow>=7.0.0 -# clip-anytorch still imports pkg_resources, removed in setuptools 81. -setuptools<81 -torch +numpy>=2.3,<3 +opencv-python-headless>=5.0.0.93,<6 +Pillow>=12.3,<13 +torch>=2.13,<3 +transformers>=5.14.1,<6 +huggingface-hub>=1.25.1,<2 diff --git a/src/vidxp/capabilities/scene/specs.py b/src/vidxp/capabilities/scene/specs.py new file mode 100644 index 0000000..920abf4 --- /dev/null +++ b/src/vidxp/capabilities/scene/specs.py @@ -0,0 +1,16 @@ +from vidxp.model_contracts import ModelSpec + + +SIGLIP2_MODEL = ModelSpec( + capability="scene", + provider="transformers", + model_id="google/siglip2-base-patch16-224", + revision="75de2d55ec2d0b4efc50b3e9ad70dba96a7b2fa2", + download_size_bytes=1_539_458_338, + weights_file="model.safetensors", + weights_sha256=( + "612923381c76ec5a9bed335d1c48827e3f2e506ac31b044b63b2031fadee6a0b" + ), + license="Apache-2.0", + weights_precision="float32", +) diff --git a/src/vidxp/capabilities/schemas.py b/src/vidxp/capabilities/schemas.py index a70b948..601818a 100644 --- a/src/vidxp/capabilities/schemas.py +++ b/src/vidxp/capabilities/schemas.py @@ -1,48 +1,16 @@ from __future__ import annotations -from typing import Any - from pydantic import Field -from vidxp.capabilities.contracts import CapabilityInput, CapabilityOutput -from vidxp.core.contracts import INDEX_SCHEMA_VERSION +from vidxp.application_models import SearchHit, SearchQuery, SearchResult +from vidxp.capabilities.contracts import CapabilityInput +from vidxp.core.identifiers import MediaId class SearchInput(CapabilityInput): - query: str = Field(min_length=1) - top_k: int = Field(default=10, gt=0) - - -class SearchHit(CapabilityOutput): - rank: int = Field(gt=0) - video_id: str = Field(min_length=1) - start: float = Field(ge=0) - end: float = Field(gt=0) - score: float - raw_distance: float - modality: str = Field(min_length=1) - source_id: str = Field(min_length=1) - metadata: dict[str, Any] = Field(default_factory=dict) - - def to_dict(self) -> dict[str, Any]: - return self.model_dump(mode="json") - - -class SearchResult(CapabilityOutput): - query_id: str = Field(min_length=1) - query: str = Field(min_length=1) - modality: str = Field(min_length=1) - hits: tuple[SearchHit, ...] = () + query: SearchQuery + top_k: int = Field(default=10, gt=0, le=100) + media_id: MediaId | None = None - def to_dict(self) -> dict[str, Any]: - return { - "schema_version": INDEX_SCHEMA_VERSION, - **self.model_dump(mode="json"), - } - def to_prediction(self) -> dict[str, list[dict[str, Any]]]: - return { - self.query_id: [ - hit.model_dump(mode="json") for hit in self.hits - ] - } +__all__ = ["SearchHit", "SearchInput", "SearchResult"] diff --git a/src/vidxp/capabilities/search.py b/src/vidxp/capabilities/search.py index 4b8a6ff..484abe6 100644 --- a/src/vidxp/capabilities/search.py +++ b/src/vidxp/capabilities/search.py @@ -11,7 +11,19 @@ IndexConfig, IndexSchemaError, ) -from vidxp.core.storage import IndexStorage +from vidxp.ports import IndexStore + + +PUBLIC_SEARCH_METADATA = frozenset( + { + "text", + "phrase_id", + "frame_index", + "timestamp", + "fps", + "duration", + } +) def distance_to_score(raw_distance: float) -> float: @@ -57,7 +69,9 @@ def _to_hits( hits = [] for rank, row in enumerate(ordered, start=1): metadata = row["metadata"] - missing = sorted(required_metadata - metadata.keys()) + missing = sorted( + (required_metadata | {"generation_id"}) - metadata.keys() + ) if missing: raise IndexSchemaError( "The saved index predates the benchmark-ready schema and must " @@ -74,14 +88,20 @@ def _to_hits( hits.append( SearchHit( rank=rank, + media_id=str(metadata["video_id"]), video_id=str(metadata["video_id"]), + generation_id=str(metadata["generation_id"]), start=start, end=end, score=distance_to_score(distance), raw_distance=distance, modality=modality, source_id=str(row["source_id"]), - metadata=metadata, + metadata={ + key: value + for key, value in metadata.items() + if key in PUBLIC_SEARCH_METADATA + }, ) ) return tuple(hits) @@ -98,7 +118,7 @@ def search_embeddings( video_id: str | None = None, query_id: str | None = None, filters: Mapping[str, Any] | None = None, - storage: IndexStorage | None = None, + storage: IndexStore, ) -> SearchResult: query = query.strip() if not query: @@ -109,19 +129,13 @@ def search_embeddings( raise ValueError( f"The {modality} modality is not present in this index run." ) - owns_storage = storage is None - store = storage or IndexStorage(config) - try: - rows = store.query( - modality, - embedding, - top_k=top_k, - video_id=video_id, - filters=filters, - ) - finally: - if owns_storage: - store.close() + rows = storage.query( + modality, + embedding, + top_k=top_k, + video_id=video_id, + filters=filters, + ) return SearchResult( query_id=query_id or stable_query_id(query, modality, config), query=query, diff --git a/src/vidxp/capabilities/visual.py b/src/vidxp/capabilities/visual.py index b7ef539..a49e2dd 100644 --- a/src/vidxp/capabilities/visual.py +++ b/src/vidxp/capabilities/visual.py @@ -5,15 +5,17 @@ from typing import Any, Protocol, Sequence from vidxp.capabilities.contracts import CapabilityIndexResult +from vidxp.capabilities.registry import CapabilityRegistry from vidxp.core.contracts import ( CancellationToken, IndexConfig, VideoSource, ) from vidxp.core.indexing_common import ProgressCallback, report_progress -from vidxp.core.storage import IndexStorage +from vidxp.ports import IndexStore, ModelRuntimePort from vidxp.core.video import ( FrameSample, + FrameSampling, FrameStreamStats, iter_frame_batches, probe_video, @@ -21,11 +23,14 @@ class VisualProcessor(Protocol): + def sampling(self, config: IndexConfig, info: Any) -> FrameSampling: ... + def batch_size(self, config: IndexConfig) -> int: ... def prepare( self, config: IndexConfig, + runtime: ModelRuntimePort, progress: ProgressCallback | None, ) -> Any: ... @@ -36,7 +41,7 @@ def process( state: Any, info: Any, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, ) -> None: ... @@ -45,7 +50,7 @@ def finalize( state: Any, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, ) -> tuple[dict[str, Any], int]: ... @@ -54,6 +59,7 @@ class _Participant: name: str processor: VisualProcessor state: Any + sampling: FrameSampling def _rgb_samples(samples) -> list[FrameSample]: @@ -72,26 +78,63 @@ def _rgb_samples(samples) -> list[FrameSample]: def _participants( names: Sequence[str], *, + info: Any, config: IndexConfig, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, progress: ProgressCallback | None, timings: dict[str, float], ) -> list[_Participant]: - from vidxp.capabilities.registry import get_capability - participants = [] for name in names: - processor = get_capability(name).index_processor + processor = registry.executor(name).index_processor if processor is None: raise ValueError( f"Capability {name!r} does not provide a visual processor." ) started = perf_counter() - state = processor.prepare(config, progress) + state = processor.prepare(config, runtime, progress) timings[name] = perf_counter() - started - participants.append(_Participant(name, processor, state)) + sampling_factory = getattr(type(processor), "sampling", None) + sampling = ( + FrameSampling(frame_stride=config.frame_stride) + if sampling_factory is None + else sampling_factory(processor, config, info) + ) + if not isinstance(sampling, FrameSampling): + raise ValueError( + f"Capability {name!r} returned invalid frame sampling." + ) + participants.append(_Participant(name, processor, state, sampling)) return participants +def _is_participant_sample( + sample: FrameSample, + participant: _Participant, +) -> bool: + return participant.sampling.includes( + sample.frame_index, + sample.timestamp, + ) + + +def _expected_sample_count( + info: Any, + participants: Sequence[_Participant], +) -> int: + return sum( + any( + participant.sampling.includes( + frame_index, + frame_index / info.fps, + ) + for participant in participants + ) + for frame_index in range(max(0, info.frame_count)) + ) + + def _consume_visual_stream( source: VideoSource, *, @@ -99,7 +142,7 @@ def _consume_visual_stream( expected: int, info: Any, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, progress: ProgressCallback | None, timings: dict[str, float], @@ -108,7 +151,9 @@ def _consume_visual_stream( stream = iter( iter_frame_batches( source.path, - frame_stride=config.frame_stride, + samplings=tuple( + participant.sampling for participant in participants + ), batch_size=max( participant.processor.batch_size(config) for participant in participants @@ -128,9 +173,16 @@ def _consume_visual_stream( timings["frame_stream"] += perf_counter() - stream_started for participant in participants: + participant_samples = [ + sample + for sample in rgb_samples + if _is_participant_sample(sample, participant) + ] + if not participant_samples: + continue processor_started = perf_counter() participant.processor.process( - rgb_samples, + participant_samples, state=participant.state, info=info, config=config, @@ -155,7 +207,7 @@ def _finalize( participants: Sequence[_Participant], *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, timings: dict[str, float], ) -> tuple[dict[str, Any], int]: summary: dict[str, Any] = {} @@ -183,8 +235,10 @@ def index_visuals( source: VideoSource, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, progress: ProgressCallback | None = None, modalities: Sequence[str] | None = None, ) -> CapabilityIndexResult: @@ -201,18 +255,19 @@ def index_visuals( started = perf_counter() info = probe_video(source.path) - expected = ( - info.frame_count + config.frame_stride - 1 - ) // config.frame_stride timings = { "frame_stream": 0.0, } participants = _participants( selected, + info=info, config=config, + registry=registry, + runtime=runtime, progress=progress, timings=timings, ) + expected = _expected_sample_count(info, participants) report_progress( progress, "visual_indexing", @@ -259,8 +314,10 @@ def index_capabilities( source: VideoSource, *, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, cancellation: CancellationToken, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, progress: ProgressCallback | None = None, modalities: Sequence[str] | None = None, ) -> CapabilityIndexResult: @@ -269,6 +326,8 @@ def index_capabilities( config=config, storage=storage, cancellation=cancellation, + registry=registry, + runtime=runtime, progress=progress, modalities=modalities, ) diff --git a/src/vidxp/capability_service.py b/src/vidxp/capability_service.py new file mode 100644 index 0000000..9b3844b --- /dev/null +++ b/src/vidxp/capability_service.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from vidxp.application_models import ( + CapabilityInfo, + CapabilityOperationInfo, + CapabilitySummary, +) +from vidxp.capabilities.registry import CapabilityRegistry + + +class CapabilityService: + """Transport-neutral capability metadata projection.""" + + def __init__(self, registry: CapabilityRegistry) -> None: + self.registry = registry + + def list(self) -> tuple[CapabilitySummary, ...]: + return tuple(self._summary(name) for name in self.registry.names()) + + def get(self, name: str) -> CapabilityInfo: + return self._info(name) + + def _summary(self, name: str) -> CapabilitySummary: + definition = self.registry.get(name) + return CapabilitySummary( + name=definition.name, + description=definition.description, + install_extra=definition.extra, + supports_indexing=definition.collection_name is not None, + prepares_models=definition.prepares_models, + provenance=self.registry.provenance(name), + ) + + def _info(self, name: str) -> CapabilityInfo: + definition = self.registry.get(name) + return CapabilityInfo( + **self._summary(name).model_dump(), + operations=tuple( + CapabilityOperationInfo( + name=operation_name, + requires_index=operation.requires_index, + input_schema=operation.input_model.model_json_schema(), + output_schema=operation.output_model.model_json_schema(), + ) + for operation_name, operation in definition.operations.items() + if operation.public + ), + ) diff --git a/src/vidxp/composition.py b/src/vidxp/composition.py new file mode 100644 index 0000000..f0c514b --- /dev/null +++ b/src/vidxp/composition.py @@ -0,0 +1,498 @@ +from __future__ import annotations + +from dataclasses import dataclass +from functools import cached_property +from pathlib import Path + +from pydantic import ValidationError + +from vidxp.application import VidXPApplication +from vidxp.application_models import ApplicationError, ErrorCategory +from vidxp.artifact_service import ArtifactQueryService, ArtifactService +from vidxp.authentication import Authenticator, create_authenticator +from vidxp.authorization import AuthorizationPolicy +from vidxp.capability_service import CapabilityService +from vidxp.capabilities.registry import ( + CapabilityRegistry, + create_capability_registry, +) +from vidxp.control_plane import ControlPlaneApplication +from vidxp.core.storage import BUNDLED_CHROMA_SERVER_URL +from vidxp.dependencies import active_requirements, packaged_requirements +from vidxp.infrastructure.local_index import ( + LOCAL_INDEX_RUNTIME_CHECKS, + SERVER_INDEX_RUNTIME_CHECKS, + LocalIndexBackend, + LocalIndexReader, +) +from vidxp.infrastructure.local_artifacts import ( + FFmpegSnippetRenderer, + LocalActorRenderer, + LocalArtifactStore, +) +from vidxp.infrastructure.local_catalog import LocalCatalog +from vidxp.infrastructure.sql_catalog import SQLCatalog +from vidxp.infrastructure.sql_snapshots import SQLSnapshotRepository +from vidxp.infrastructure.local_media import FFprobeMediaProbe, LocalMediaStore +from vidxp.infrastructure.local_snapshots import LocalSnapshotRepository +from vidxp.infrastructure.dbos_jobs import DBOSJobBackend +from vidxp.infrastructure.local_worker import LocalWorkerSupervisor +from vidxp.job_service import JobService +from vidxp.media_service import MediaService +from vidxp.readiness_service import ReadinessService +from vidxp.read_job_planner import LocalReadJobPlanner +from vidxp.runtime import ModelRuntime, RuntimeBackendUnavailableError +from vidxp.repositories import ( + RepositoryConfig, + RepositoryConfigError, + RepositoryRegistry, + resolve_repository, +) +from vidxp.settings import ApplicationMode, VidXPSettings +from vidxp.upload_service import RemoteUploadService +from vidxp.workflow_runtime import ( + workflow_application_version, + workflow_database_url, +) + + +class LocalApplicationContext: + """Lazy local composition so lightweight CLI commands stay lightweight.""" + + def __init__( + self, + *, + repositories: RepositoryRegistry, + repository: RepositoryConfig, + settings: VidXPSettings | None = None, + application: VidXPApplication | None = None, + jobs: JobService | None = None, + ) -> None: + if settings is None and (application is None or jobs is None): + raise ValueError( + "Lazy local composition requires settings or composed services." + ) + self.repositories = repositories + self.repository = repository + self._settings = settings + if application is not None: + self.__dict__["application"] = application + if jobs is not None: + self.__dict__["jobs"] = jobs + + @cached_property + def application(self) -> VidXPApplication: + assert self._settings is not None + return create_application(self._settings) + + @property + def settings(self) -> VidXPSettings: + if self._settings is not None: + return self._settings + return settings_for_repository(self.repository) + + @cached_property + def jobs(self) -> JobService: + assert self._settings is not None + return create_job_service(self._settings) + + def close(self) -> None: + jobs = self.__dict__.get("jobs") + if jobs is not None: + jobs.close() + + +@dataclass(frozen=True, kw_only=True) +class ControlPlaneContext: + application: ControlPlaneApplication + jobs: JobService + authorization: AuthorizationPolicy + settings: VidXPSettings + catalog: SQLCatalog | None = None + uploads: RemoteUploadService | None = None + + def close(self) -> None: + try: + if self.settings.mode != ApplicationMode.server: + self.jobs.stop_worker() + finally: + try: + self.jobs.close() + finally: + if self.catalog is not None: + self.catalog.close() + + +@dataclass(frozen=True, kw_only=True) +class HttpApplicationContext(ControlPlaneContext): + readiness: ReadinessService + authenticator: Authenticator + + +@dataclass(frozen=True) +class UploadHookContext: + jobs: JobService + authenticator: Authenticator + authorization: AuthorizationPolicy + settings: VidXPSettings + catalog: SQLCatalog + uploads: RemoteUploadService + + def close(self) -> None: + self.jobs.close() + self.catalog.close() + + +@dataclass(frozen=True) +class _ControlPlaneComponents: + registry: CapabilityRegistry + catalog: SQLCatalog + media: MediaService + artifact_store: LocalArtifactStore + probe: FFprobeMediaProbe + snapshots: LocalSnapshotRepository + + +def _server_chroma_url(settings: VidXPSettings) -> str | None: + if settings.mode != ApplicationMode.server: + return None + return BUNDLED_CHROMA_SERVER_URL + + +def _create_control_plane_components( + settings: VidXPSettings, +) -> _ControlPlaneComponents: + settings.layout.ensure_local_directories() + server_mode = settings.mode == ApplicationMode.server + registry = create_capability_registry( + external=settings.external_capabilities, + allowlist=settings.capability_allowlist, + platform_runtime_checks=( + SERVER_INDEX_RUNTIME_CHECKS + if server_mode + else LOCAL_INDEX_RUNTIME_CHECKS + ), + storage_requirements=( + active_requirements( + packaged_requirements( + "vidxp", + "requirements/server-storage.txt", + ) + ) + if server_mode + else None + ), + ) + catalog = ( + SQLCatalog( + workflow_database_url(settings), + initialize=False, + ) + if settings.mode == ApplicationMode.server + else LocalCatalog(settings.layout.catalog) + ) + probe = FFprobeMediaProbe(settings.ffprobe_executable) + snapshots = ( + SQLSnapshotRepository( + settings.layout.indexes, + engine=catalog.engine, + ) + if settings.mode == ApplicationMode.server + else LocalSnapshotRepository(settings.layout.indexes) + ) + return _ControlPlaneComponents( + registry=registry, + catalog=catalog, + media=MediaService( + settings=settings, + catalog=catalog, + store=LocalMediaStore( + settings.layout.media, + max_bytes=( + settings.upload_max_bytes + if settings.mode == ApplicationMode.server + else settings.max_local_import_bytes + ), + ), + probe=probe, + ), + artifact_store=LocalArtifactStore(settings.layout.artifacts), + probe=probe, + snapshots=snapshots, + ) + + +def settings_for_repository( + repository: RepositoryConfig, + *, + data_directory: str | Path | None = None, +) -> VidXPSettings: + values = { + "mode": ApplicationMode.local, + "repository_root": repository.index_directory, + } + if data_directory is not None: + values["data_dir"] = Path(data_directory) + if repository.device is not None: + values["runtime_backend"] = repository.device + return VidXPSettings(**values) + + +def create_application( + settings: VidXPSettings | None = None, +) -> VidXPApplication: + active_settings = settings or VidXPSettings() + components = _create_control_plane_components(active_settings) + try: + runtime = ModelRuntime( + active_settings, + allowed_specs=components.registry.model_specs(), + ) + except RuntimeBackendUnavailableError as exc: + raise ApplicationError( + "runtime_unavailable", + ErrorCategory.unavailable, + "The requested runtime backend is unavailable.", + ) from exc + backend = LocalIndexBackend( + components.registry, + runtime, + active_settings.layout, + chroma_server_url=_server_chroma_url(active_settings), + snapshot_repository=components.snapshots, + ) + artifacts = ArtifactService( + catalog=components.catalog, + store=components.artifact_store, + media=components.media, + probe=components.probe, + actor_renderer=LocalActorRenderer(), + snippet_renderer=FFmpegSnippetRenderer( + active_settings.ffmpeg_executable + ), + max_snippet_duration_seconds=( + active_settings.max_snippet_duration_seconds + ), + ) + upload_service = ( + RemoteUploadService( + settings=active_settings, + catalog=components.catalog, + media=components.media, + ) + if active_settings.mode == ApplicationMode.server + else None + ) + query_model = None + if ( + active_settings.slm_base_url is not None + and active_settings.slm_model is not None + ): + from vidxp.infrastructure.ollama_query import OllamaQueryModel + + query_model = OllamaQueryModel( + base_url=active_settings.slm_base_url, + model_name=active_settings.slm_model, + timeout_seconds=active_settings.slm_timeout_seconds, + output_retries=active_settings.slm_output_retries, + ) + return VidXPApplication( + settings=active_settings, + layout=active_settings.layout, + registry=components.registry, + runtime=runtime, + index_backend=backend, + media=components.media, + artifacts=artifacts, + index_status=backend.repository.status, + completed_upload_importer=( + upload_service.import_completed + if upload_service is not None + else None + ), + query_model=query_model, + ) + + +def create_job_service( + settings: VidXPSettings, + *, + catalog: SQLCatalog | None = None, + snapshots: LocalSnapshotRepository | None = None, + include_read_planner: bool = True, +) -> JobService: + settings.layout.ensure_local_directories() + before_access = None + health_check = None + stop_executor = None + if settings.mode != ApplicationMode.server: + supervisor = LocalWorkerSupervisor(settings) + before_access = supervisor.ensure_running + health_check = supervisor.health + stop_executor = supervisor.stop + return JobService( + settings=settings, + backend=DBOSJobBackend( + system_database_url=( + None + if settings.mode == ApplicationMode.server + and catalog is not None + else workflow_database_url(settings) + ), + system_database_engine=( + catalog.engine + if settings.mode == ApplicationMode.server + and catalog is not None + else None + ), + application_version=workflow_application_version(), + before_access=before_access, + health_check=health_check, + stop_executor=stop_executor, + ), + read_planner=( + LocalReadJobPlanner( + layout=settings.layout, + index=LocalIndexReader( + settings.layout, + chroma_server_url=_server_chroma_url(settings), + snapshot_repository=snapshots, + ), + ) + if include_read_planner + else None + ), + ) + + +def create_control_plane_application( + settings: VidXPSettings | None = None, +) -> ControlPlaneContext: + active_settings = settings or VidXPSettings() + components = _create_control_plane_components(active_settings) + application = ControlPlaneApplication( + layout=active_settings.layout, + capabilities=CapabilityService(components.registry), + media=components.media, + artifacts=ArtifactQueryService( + catalog=components.catalog, + store=components.artifact_store, + ), + index_status=components.snapshots.status, + model_cache=active_settings.model_cache, + ) + jobs = create_job_service( + active_settings, + catalog=components.catalog, + snapshots=components.snapshots, + ) + uploads = ( + RemoteUploadService( + settings=active_settings, + catalog=components.catalog, + media=components.media, + jobs=jobs, + ) + if active_settings.mode == ApplicationMode.server + else None + ) + return ControlPlaneContext( + application=application, + jobs=jobs, + authorization=AuthorizationPolicy(), + settings=active_settings, + catalog=components.catalog, + uploads=uploads, + ) + + +def create_http_application( + settings: VidXPSettings | None = None, +) -> HttpApplicationContext: + active_settings = settings or VidXPSettings() + active_settings.validate_http_server() + control = create_control_plane_application(active_settings) + authenticator = create_authenticator(active_settings) + return HttpApplicationContext( + application=control.application, + jobs=control.jobs, + authorization=control.authorization, + settings=control.settings, + catalog=control.catalog, + uploads=control.uploads, + readiness=ReadinessService( + application=control.application, + jobs=control.jobs, + authenticator=authenticator, + ), + authenticator=authenticator, + ) + + +def create_upload_hook_context( + settings: VidXPSettings | None = None, +) -> UploadHookContext: + active_settings = settings or VidXPSettings() + active_settings.validate_http_server() + if active_settings.mode != ApplicationMode.server: + raise ValueError("The tusd hook service requires server mode.") + catalog = SQLCatalog( + workflow_database_url(active_settings), + initialize=False, + ) + try: + jobs = create_job_service( + active_settings, + catalog=catalog, + include_read_planner=False, + ) + uploads = RemoteUploadService( + settings=active_settings, + catalog=catalog, + media=None, + jobs=jobs, + ) + return UploadHookContext( + jobs=jobs, + authenticator=create_authenticator(active_settings), + authorization=AuthorizationPolicy(), + settings=active_settings, + catalog=catalog, + uploads=uploads, + ) + except Exception: + catalog.close() + raise + + +def create_local_application( + *, + registry_path: str | Path | None = None, + repository_name: str | None = None, + index_directory: str | Path | None = None, + data_directory: str | Path | None = None, + device: str | None = None, +) -> LocalApplicationContext: + try: + repositories, repository = resolve_repository( + registry_path=registry_path, + name=repository_name, + index_directory=index_directory, + data_directory=data_directory, + device=device, + ) + settings = settings_for_repository( + repository, + data_directory=data_directory, + ) + except (RepositoryConfigError, ValidationError) as exc: + raise ApplicationError( + "configuration_invalid", + ErrorCategory.validation, + "The repository or runtime configuration is invalid.", + ) from exc + return LocalApplicationContext( + repositories=repositories, + repository=repository, + settings=settings, + ) diff --git a/src/vidxp/control_plane.py b/src/vidxp/control_plane.py new file mode 100644 index 0000000..991f48c --- /dev/null +++ b/src/vidxp/control_plane.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Callable + +from vidxp.application_boundary import application_boundary +from vidxp.application_models import ( + Artifact, + CapabilityInfo, + CapabilitySummary, + ComponentReadiness, + DependencyCheckResult, + IndexStatus, + InvalidRequestError, + ListMediaCommand, + MediaAsset, + MediaPage, + ModelUnavailableError, + ResourceNotFoundError, + RuntimeReadiness, +) +from vidxp.artifact_service import ArtifactQueryService +from vidxp.capabilities.contracts import CapabilityRequestError +from vidxp.capability_service import CapabilityService +from vidxp.core.media import QuarantinedMedia +from vidxp.index_state import INDEX_STATUS_SCHEMA +from vidxp.media_service import MediaService +from vidxp.ports import LocalFileResource +from vidxp.repository_layout import RepositoryLayout + + +class ControlPlaneApplication: + """Model-free application facade used by HTTP and future remote adapters.""" + + def __init__( + self, + *, + layout: RepositoryLayout, + capabilities: CapabilityService, + media: MediaService, + artifacts: ArtifactQueryService, + index_status: Callable[[], dict | None], + model_cache: Path, + ) -> None: + self.layout = layout + self.capabilities = capabilities + self.media = media + self.artifacts = artifacts + self._read_index_status = index_status + self.model_cache = model_cache + + @application_boundary + def import_uploaded_media( + self, + *, + staged_path: Path, + original_filename: str, + declared_mime_type: str | None, + request_key: str | None = None, + ) -> MediaAsset: + self.layout.ensure_local_directories() + return self.media.import_quarantined( + QuarantinedMedia( + path=staged_path, + original_filename=original_filename, + declared_mime_type=declared_mime_type, + ), + request_key=request_key, + ) + + @application_boundary + def list_capabilities(self) -> tuple[CapabilitySummary, ...]: + return self.capabilities.list() + + @application_boundary + def get_capability(self, name: str) -> CapabilityInfo: + try: + return self.capabilities.get(name) + except CapabilityRequestError as exc: + raise ResourceNotFoundError("capability") from exc + + @application_boundary + def index_status(self) -> IndexStatus: + stored = self._read_index_status() + payload = ( + dict(stored) + if stored is not None + else { + "schema_version": INDEX_STATUS_SCHEMA, + "state": "missing", + "stage": "status", + "message": "No local video index was found.", + } + ) + return IndexStatus.model_validate(payload) + + @application_boundary + def get_media(self, media_id: str) -> MediaAsset: + return self.media.get(media_id) + + @application_boundary + def list_media(self, command: ListMediaCommand) -> MediaPage: + try: + return self.media.list(command) + except ValueError as exc: + raise InvalidRequestError() from exc + + @application_boundary + def open_media_content(self, media_id: str) -> LocalFileResource: + return self.media.content(media_id) + + @application_boundary + def get_artifact(self, artifact_id: str) -> Artifact: + return self.artifacts.get(artifact_id) + + @application_boundary + def open_artifact_content(self, artifact_id: str) -> LocalFileResource: + return self.artifacts.content(artifact_id) + + def control_plane_readiness(self) -> tuple[ComponentReadiness, ...]: + components: list[ComponentReadiness] = [] + try: + self.media.list(ListMediaCommand(page_size=1)) + except Exception: + components.append( + ComponentReadiness( + name="catalog", + ready=False, + message="The media catalog is unavailable.", + ) + ) + else: + components.append( + ComponentReadiness( + name="catalog", + ready=True, + message="The media catalog is available.", + ) + ) + try: + self.index_status() + except Exception: + components.append( + ComponentReadiness( + name="index", + ready=False, + message="The index catalog is unavailable.", + ) + ) + else: + components.append( + ComponentReadiness( + name="index", + ready=True, + message="The index catalog is available.", + ) + ) + return tuple(components) + + def model_readiness( + self, + modalities: tuple[str, ...] | None = None, + ) -> DependencyCheckResult: + selected = ( + self.capabilities.registry.preparable_names() + if modalities is None + else self.capabilities.registry.validate_names(modalities) + ) + checks = self.capabilities.registry.model_checks( + selected, + cache=self.model_cache, + ) + return DependencyCheckResult( + ok=all(check.ok for check in checks), + modalities=selected, + checks=checks, + ) + + def require_models(self, modalities: tuple[str, ...]) -> None: + readiness = self.model_readiness(modalities) + missing = next((check for check in readiness.checks if not check.ok), None) + if missing is not None: + raise ModelUnavailableError(missing.capability) + + def runtime_readiness(self) -> RuntimeReadiness: + components = self.control_plane_readiness() + models = self.model_readiness() + return RuntimeReadiness( + ready=( + all(component.ready for component in components) + and models.ok + ), + runtime=None, + components=components, + dependencies=models, + ) diff --git a/src/vidxp/core/artifacts.py b/src/vidxp/core/artifacts.py new file mode 100644 index 0000000..f305d36 --- /dev/null +++ b/src/vidxp/core/artifacts.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from enum import StrEnum +from pathlib import Path +from typing import Literal + +from pydantic import ( + AwareDatetime, + BaseModel, + ConfigDict, + Field, + field_validator, +) + +from vidxp.core.identifiers import ( + ArtifactId, + IndexGenerationId, + JobId, + MediaId, + MimeType, + Sha256, +) +from vidxp.core.storage_keys import validate_storage_key + + +ARTIFACT_SCHEMA_VERSION = 1 + + +class ArtifactIntegrityError(RuntimeError): + """Raised when managed artifact bytes no longer match catalog authority.""" + + +class ArtifactRenderError(RuntimeError): + """Raised when an artifact renderer cannot produce the requested output.""" + + +class ArtifactRendererUnavailableError(RuntimeError): + """Raised when the configured artifact renderer is unavailable.""" + + +class ArtifactKind(StrEnum): + actor_overlay = "actor_overlay" + snippet = "snippet" + + +class ArtifactState(StrEnum): + ready = "ready" + + +class _ArtifactModel(BaseModel): + model_config = ConfigDict( + extra="forbid", + frozen=True, + allow_inf_nan=False, + strict=True, + ) + + +class ArtifactRecord(_ArtifactModel): + """Authoritative internal artifact entry.""" + + schema_version: Literal[ARTIFACT_SCHEMA_VERSION] = ARTIFACT_SCHEMA_VERSION + artifact_id: ArtifactId + media_id: MediaId + generation_id: IndexGenerationId | None = None + request_key: Sha256 + job_id: JobId | None = None + kind: ArtifactKind + profile: str = Field(min_length=1) + mime_type: MimeType + byte_size: int = Field(gt=0) + sha256: Sha256 + storage_key: str = Field(min_length=1) + state: ArtifactState = ArtifactState.ready + created_at: AwareDatetime + expires_at: AwareDatetime | None = None + + @field_validator("storage_key") + @classmethod + def _validate_storage_key(cls, value: str) -> str: + return validate_storage_key(value) + + +class StagedArtifact(_ArtifactModel): + artifact_id: ArtifactId + path: Path + + +class StoredArtifact(_ArtifactModel): + sha256: Sha256 + byte_size: int = Field(gt=0) + storage_key: str = Field(min_length=1) + local_path: Path diff --git a/src/vidxp/core/contracts.py b/src/vidxp/core/contracts.py index cece2ce..406337f 100644 --- a/src/vidxp/core/contracts.py +++ b/src/vidxp/core/contracts.py @@ -11,8 +11,8 @@ from urllib.parse import quote -INDEX_SCHEMA_VERSION = 3 -MANIFEST_SCHEMA_VERSION = 1 +INDEX_SCHEMA_VERSION = 6 +MANIFEST_SCHEMA_VERSION = 2 class IndexCancelledError(RuntimeError): @@ -30,6 +30,16 @@ def _require_identifier(label: str, value: str) -> str: return value +def _require_sha256(label: str, value: str) -> str: + checksum = str(value).lower() + if ( + len(checksum) != 64 + or any(character not in string.hexdigits for character in checksum) + ): + raise ValueError(f"{label} must be a 64-character SHA-256 value.") + return checksum + + def _filesystem_component(value: str) -> str: value = _require_identifier("path component", value) if value in {".", ".."}: @@ -50,6 +60,8 @@ def stable_source_id( video_id: str, modality: str, local_id: str | int, + *, + generation_id: str | None = None, ) -> str: """Return an escaped, deterministic record ID. @@ -59,6 +71,9 @@ def stable_source_id( values = (run_id, video_id, modality, str(local_id)) labels = ("run_id", "video_id", "modality", "local_id") + if generation_id is not None: + values = (generation_id, *values) + labels = ("generation_id", *labels) encoded = [ quote(_require_identifier(label, value), safe="") for label, value in zip(labels, values) @@ -82,6 +97,10 @@ class IndexConfig: ) output_root: str | Path = "benchmark_runs" storage_directory: str | Path | None = None + generation_directory: str | Path | None = None + generation_id: str | None = None + snapshot_id: str | None = None + snapshot_sha256: str | None = None collection_names: Mapping[str, str] = field( default_factory=dict ) @@ -94,10 +113,26 @@ def __post_init__(self) -> None: "storage_directory", str(self.storage_directory), ) + if self.generation_directory is not None: + object.__setattr__( + self, + "generation_directory", + str(self.generation_directory), + ) for label in ("dataset", "split", "run_id"): _require_identifier(label, getattr(self, label)) if self.video_id is not None: _require_identifier("video_id", self.video_id) + if self.generation_id is not None: + _require_identifier("generation_id", self.generation_id) + if self.snapshot_id is not None: + _require_identifier("snapshot_id", self.snapshot_id) + if self.snapshot_sha256 is not None: + object.__setattr__( + self, + "snapshot_sha256", + _require_sha256("snapshot_sha256", self.snapshot_sha256), + ) modalities = tuple(dict.fromkeys(self.enabled_modalities)) if not modalities: @@ -170,20 +205,20 @@ def __post_init__(self) -> None: @classmethod def local(cls, **changes: Any) -> "IndexConfig": - """Return the single-video configuration used by the existing CLI/UI.""" + """Return the default configuration for a local repository operation.""" defaults = { "storage_directory": "chroma_data", } if "enabled_modalities" not in changes: - from vidxp.capabilities.registry import index_capability_names - - defaults["enabled_modalities"] = index_capability_names() + defaults["enabled_modalities"] = ("dialogue", "scene", "actor") defaults.update(changes) return cls(**defaults) @property def run_directory(self) -> Path: + if self.generation_directory is not None: + return Path(self.generation_directory) if self.storage_directory is not None: return Path(self.storage_directory) return ( @@ -212,7 +247,7 @@ def record_identity( raise ValueError( f"Capability {modality!r} is not enabled for this run." ) - return { + identity = { "dataset": self.dataset, "split": self.split, "run_id": self.run_id, @@ -220,6 +255,9 @@ def record_identity( "modality": modality, "source_id": source_id, } + if self.generation_id is not None: + identity["generation_id"] = self.generation_id + return identity def options_for(self, capability: str) -> dict[str, Any]: if capability not in self.enabled_modalities: @@ -238,7 +276,16 @@ def to_dict(self) -> dict[str, Any]: def fingerprint(self) -> str: payload = asdict(self) - for excluded in ("video_id", "output_root", "storage_directory"): + for excluded in ( + "video_id", + "device", + "output_root", + "storage_directory", + "generation_directory", + "generation_id", + "snapshot_id", + "snapshot_sha256", + ): payload.pop(excluded, None) encoded = json.dumps( payload, @@ -264,13 +311,11 @@ def __post_init__(self) -> None: if self.path is None and self.transcript is None: raise ValueError("A video path or timestamped transcript is required.") if self.checksum is not None: - checksum = str(self.checksum).lower() - if ( - len(checksum) != 64 - or any(character not in string.hexdigits for character in checksum) - ): - raise ValueError("checksum must be a 64-character SHA-256 value.") - object.__setattr__(self, "checksum", checksum) + object.__setattr__( + self, + "checksum", + _require_sha256("checksum", self.checksum), + ) @dataclass(frozen=True) diff --git a/src/vidxp/core/cursors.py b/src/vidxp/core/cursors.py new file mode 100644 index 0000000..518d5e2 --- /dev/null +++ b/src/vidxp/core/cursors.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import base64 +import binascii +import json +from typing import Any, Mapping + + +MAX_CURSOR_OFFSET = (1 << 63) - 1 + + +class CursorError(ValueError): + """Raised when an opaque VidXP cursor cannot be validated.""" + + +def encode_cursor(scope: str, position: Mapping[str, Any]) -> str: + payload = { + "version": 1, + "scope": scope, + **position, + } + encoded = json.dumps( + payload, + separators=(",", ":"), + sort_keys=True, + ).encode() + return base64.urlsafe_b64encode(encoded).decode() + + +def decode_cursor(cursor: str, scope: str) -> dict[str, Any]: + try: + if not isinstance(cursor, str): + raise TypeError + encoded = cursor.encode("ascii") + decoded = base64.b64decode( + encoded, + altchars=b"-_", + validate=True, + ) + if base64.urlsafe_b64encode(decoded) != encoded: + raise CursorError("The cursor is invalid.") + payload = json.loads(decoded.decode()) + except ( + CursorError, + TypeError, + UnicodeEncodeError, + UnicodeDecodeError, + ValueError, + json.JSONDecodeError, + binascii.Error, + ) as exc: + raise CursorError("The cursor is invalid.") from exc + if ( + not isinstance(payload, dict) + or payload.get("version") != 1 + or payload.get("scope") != scope + ): + raise CursorError("The cursor is invalid.") + return payload + + +def encode_offset_cursor( + offset: int, + *, + scope: str, + has_more: bool = True, +) -> str | None: + if not has_more: + return None + if ( + not isinstance(offset, int) + or isinstance(offset, bool) + or offset < 0 + or offset > MAX_CURSOR_OFFSET + ): + raise CursorError("The cursor offset is invalid.") + return encode_cursor(scope, {"offset": offset}) + + +def decode_offset_cursor(cursor: str | None, *, scope: str) -> int: + if cursor is None: + return 0 + payload = decode_cursor(cursor, scope) + offset = payload.get("offset") + if ( + not isinstance(offset, int) + or isinstance(offset, bool) + or offset < 0 + or offset > MAX_CURSOR_OFFSET + ): + raise CursorError("The cursor offset is invalid.") + return offset diff --git a/src/vidxp/core/generations.py b/src/vidxp/core/generations.py new file mode 100644 index 0000000..690db5b --- /dev/null +++ b/src/vidxp/core/generations.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import ( + AwareDatetime, + BaseModel, + ConfigDict, + Field, + JsonValue, + NonNegativeFloat, + NonNegativeInt, + model_validator, +) + +from vidxp.core.contracts import ( + INDEX_SCHEMA_VERSION, + MANIFEST_SCHEMA_VERSION, +) +from vidxp.core.identifiers import Identifier, IndexGenerationId, Sha256 + + +class _GenerationManifestModel(BaseModel): + model_config = ConfigDict( + allow_inf_nan=False, + extra="forbid", + frozen=True, + strict=True, + ) + + +class _GenerationInput(_GenerationManifestModel): + sha256: Sha256 + checksums: dict[Identifier, Sha256] + size: NonNegativeInt | None + source_name: str | None + path: str | None + metadata: dict[str, JsonValue] + + +class _GenerationStage(_GenerationManifestModel): + video_id: Identifier + stage: Identifier + seconds: NonNegativeFloat + stats: dict[str, JsonValue] + recorded_at: AwareDatetime + + +class _CompletedGenerationVideo(_GenerationManifestModel): + state: Literal["complete"] + started_at: AwareDatetime + stages: dict[Identifier, _GenerationStage] + completed_at: AwareDatetime + summary: dict[str, JsonValue] + + +class CompletedGenerationManifest(_GenerationManifestModel): + manifest_schema_version: Literal[MANIFEST_SCHEMA_VERSION] + index_schema_version: Literal[INDEX_SCHEMA_VERSION] + dataset: Identifier + split: Identifier + run_id: Identifier + generation_id: IndexGenerationId + state: Literal["complete"] + created_at: AwareDatetime + updated_at: AwareDatetime + completed_at: AwareDatetime + config_fingerprint: Sha256 + execution_fingerprint: Sha256 + configuration: dict[str, JsonValue] + models: dict[str, JsonValue] + git: dict[str, JsonValue] + environment: dict[str, JsonValue] + inputs: dict[Identifier, _GenerationInput] = Field(min_length=1, max_length=1) + videos: dict[Identifier, _CompletedGenerationVideo] = Field( + min_length=1, + max_length=1, + ) + completed_videos: tuple[Identifier, ...] = Field( + min_length=1, + max_length=1, + ) + failed_videos: tuple[Identifier, ...] = Field(max_length=0) + interrupted_videos: tuple[Identifier, ...] = Field(max_length=0) + processed_frames: NonNegativeInt + record_counts: dict[Identifier, NonNegativeInt] + store_size_bytes_at_commit: NonNegativeInt | None + + @model_validator(mode="after") + def _validate_completed_generation(self) -> CompletedGenerationManifest: + input_ids = set(self.inputs) + video_ids = set(self.videos) + completed_ids = set(self.completed_videos) + if input_ids != video_ids or input_ids != completed_ids: + raise ValueError( + "inputs, videos, and completed_videos must identify the same " + "single media item" + ) + + configured = self.configuration.get("enabled_modalities") + if ( + not isinstance(configured, list) + or not configured + or any(not isinstance(value, str) or not value for value in configured) + or len(configured) != len(set(configured)) + ): + raise ValueError( + "configuration.enabled_modalities must be a non-empty list " + "of unique identifiers" + ) + if set(self.record_counts) != set(configured): + raise ValueError( + "record_counts keys must exactly match enabled_modalities" + ) + return self diff --git a/src/vidxp/core/identifiers.py b/src/vidxp/core/identifiers.py new file mode 100644 index 0000000..8305094 --- /dev/null +++ b/src/vidxp/core/identifiers.py @@ -0,0 +1,80 @@ +from typing import Annotated, TypeAlias +from uuid import UUID + +from pydantic import AfterValidator, StringConstraints + + +Identifier: TypeAlias = Annotated[ + str, + StringConstraints( + strip_whitespace=True, + min_length=1, + max_length=255, + pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]*$", + ), +] +ActorClusterId: TypeAlias = Annotated[ + str, + StringConstraints( + strip_whitespace=True, + min_length=1, + max_length=512, + pattern=r"^[A-Za-z0-9._~:%-]+$", + ), +] + + +def _require_uuid4_hex(value: str) -> str: + identifier = UUID(hex=value) + if identifier.version != 4 or identifier.hex != value: + raise ValueError("identifier must be lowercase UUID4 hex") + return value + + +def _require_workflow_uuid(value: str) -> str: + identifier = UUID(value) + if ( + identifier.version not in {4, 7} + or value not in {identifier.hex, str(identifier)} + ): + raise ValueError("identifier must be a lowercase UUID4 or UUID7 string") + return value + + +Uuid4Hex: TypeAlias = Annotated[ + str, + StringConstraints(pattern=r"^[0-9a-f]{32}$"), + AfterValidator(_require_uuid4_hex), +] +WorkflowUuid: TypeAlias = Annotated[ + str, + StringConstraints( + pattern=( + r"^(?:[0-9a-f]{12}[47][0-9a-f]{19}|" + r"[0-9a-f]{8}-[0-9a-f]{4}-[47][0-9a-f]{3}-" + r"[89ab][0-9a-f]{3}-[0-9a-f]{12})$" + ) + ), + AfterValidator(_require_workflow_uuid), +] +Sha256: TypeAlias = Annotated[ + str, + StringConstraints(pattern=r"^[0-9a-f]{64}$"), +] +MimeType: TypeAlias = Annotated[ + str, + StringConstraints( + strip_whitespace=True, + min_length=3, + max_length=127, + pattern=r"^[A-Za-z0-9!#$&^_.+-]+/[A-Za-z0-9!#$&^_.+-]+$", + ), +] + +MediaId: TypeAlias = Uuid4Hex +VideoId: TypeAlias = MediaId +IndexGenerationId: TypeAlias = Uuid4Hex +IndexSnapshotId: TypeAlias = Uuid4Hex +JobId: TypeAlias = WorkflowUuid +ArtifactId: TypeAlias = Uuid4Hex +UploadIntentId: TypeAlias = Uuid4Hex diff --git a/src/vidxp/core/manifest.py b/src/vidxp/core/manifest.py index d2a35f0..f0790f7 100644 --- a/src/vidxp/core/manifest.py +++ b/src/vidxp/core/manifest.py @@ -2,9 +2,11 @@ import hashlib import json +import os import platform import subprocess import sys +import tempfile import time from datetime import datetime, timezone from importlib.metadata import PackageNotFoundError, version @@ -12,8 +14,7 @@ from typing import Any, Mapping from vidxp.capabilities.registry import ( - get_capability, - runtime_distributions, + CapabilityRegistry, ) from vidxp.core.contracts import ( INDEX_SCHEMA_VERSION, @@ -21,6 +22,7 @@ IndexConfig, VideoSource, ) +from vidxp.ports import ModelRuntimePort MANIFEST_FILE = "manifest.json" @@ -36,25 +38,58 @@ def utc_now() -> str: def write_json_atomic(path: Path, payload: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_suffix(path.suffix + ".tmp") - temporary.write_text( - json.dumps( - payload, - ensure_ascii=False, - indent=2, - sort_keys=True, - ) - + "\n", - encoding="utf-8", - ) - for attempt in range(5): - try: - temporary.replace(path) - return - except PermissionError: - if attempt == 4: - raise - time.sleep(0.05 * (attempt + 1)) + temporary: Path | None = None + try: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=path.parent, + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as destination: + temporary = Path(destination.name) + destination.write( + json.dumps( + payload, + ensure_ascii=False, + indent=2, + sort_keys=True, + ) + + "\n" + ) + destination.flush() + os.fsync(destination.fileno()) + + for attempt in range(5): + try: + temporary.replace(path) + break + except OSError: + if attempt == 4: + raise + time.sleep(0.05 * (attempt + 1)) + + sync_parent_directory(path.parent) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def sync_parent_directory(path: Path) -> None: + if os.name == "nt": + return + descriptor: int | None = None + try: + descriptor = os.open(path, os.O_RDONLY) + os.fsync(descriptor) + except OSError: + # The replacement is already visible. Some supported filesystems + # reject directory fsync, which cannot roll that replacement back. + pass + finally: + if descriptor is not None: + os.close(descriptor) def _append_jsonl(path: Path, payload: Mapping[str, Any]) -> None: @@ -153,9 +188,11 @@ def implementation_digest() -> str: return digest.hexdigest() -def dependency_versions() -> dict[str, str | None]: +def dependency_versions( + registry: CapabilityRegistry, +) -> dict[str, str | None]: versions: dict[str, str | None] = {} - for package in runtime_distributions(): + for package in registry.runtime_distributions(): try: versions[package] = version(package) except PackageNotFoundError: @@ -163,7 +200,9 @@ def dependency_versions() -> dict[str, str | None]: return versions -def execution_state() -> dict[str, Any]: +def execution_state( + registry: CapabilityRegistry, +) -> dict[str, Any]: try: package_version = version("vidxp") except PackageNotFoundError: @@ -174,7 +213,7 @@ def execution_state() -> dict[str, Any]: "package_version": package_version, "python": sys.version, "platform": platform.platform(), - "dependencies": dependency_versions(), + "dependencies": dependency_versions(registry), } @@ -193,8 +232,16 @@ def execution_fingerprint(state: Mapping[str, Any]) -> str: class ManifestStore: - def __init__(self, config: IndexConfig): + def __init__( + self, + config: IndexConfig, + *, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, + ): self.config = config + self.registry = registry + self.runtime = runtime self.run_directory = config.run_directory self.manifest_path = self.run_directory / MANIFEST_FILE self.timings_path = self.run_directory / TIMINGS_FILE @@ -214,10 +261,13 @@ def _model_manifest( tuple[str, VideoSource, str, Mapping[str, str]] ], ) -> dict[str, Any]: - models: dict[str, Any] = {"device": self.config.device} + models: dict[str, Any] = { + "device": self.config.device, + "runtime": self.runtime.describe(), + } source_values = tuple(source for _, source, _, _ in sources) for name in self.config.enabled_modalities: - manifest = get_capability(name).model_manifest + manifest = self.registry.executor(name).model_manifest if manifest is not None: models.update(manifest(self.config, source_values)) return models @@ -230,7 +280,7 @@ def initialize( ) -> dict[str, Any]: self.run_directory.mkdir(parents=True, exist_ok=True) config_fingerprint = self.config.fingerprint() - current_execution = execution_state() + current_execution = execution_state(self.registry) current_execution_fingerprint = execution_fingerprint( current_execution ) @@ -257,6 +307,7 @@ def initialize( "dataset": self.config.dataset, "split": self.config.split, "run_id": self.config.run_id, + "generation_id": self.config.generation_id, "state": "running", "created_at": utc_now(), "updated_at": utc_now(), @@ -272,7 +323,8 @@ def initialize( "failed_videos": [], "interrupted_videos": [], "processed_frames": 0, - "index_size_bytes": 0, + "record_counts": {}, + "store_size_bytes_at_commit": None, } if reset: for path in ( @@ -319,6 +371,9 @@ def read(self) -> dict[str, Any]: def write(self, manifest: Mapping[str, Any]) -> None: write_json_atomic(self.manifest_path, manifest) + def _refresh_runtime(self, manifest: dict[str, Any]) -> None: + manifest["models"]["runtime"] = self.runtime.describe() + def checkpoint(self, video_id: str) -> dict[str, Any] | None: path = self._checkpoint_path(video_id) if not path.is_file(): @@ -430,6 +485,7 @@ def complete_video( if video_id not in manifest["completed_videos"]: manifest["completed_videos"].append(video_id) manifest["completed_videos"].sort() + self._refresh_runtime(manifest) manifest["updated_at"] = utc_now() self.write(manifest) @@ -449,6 +505,7 @@ def fail_video(self, video_id: str, stage: str, error: str) -> None: manifest["failed_videos"].append(video_id) manifest["failed_videos"].sort() manifest["state"] = "failed" + self._refresh_runtime(manifest) manifest["updated_at"] = utc_now() self.write(manifest) @@ -465,16 +522,24 @@ def interrupt_video(self, video_id: str, stage: str) -> None: manifest["interrupted_videos"].append(video_id) manifest["interrupted_videos"].sort() manifest["state"] = "interrupted" + self._refresh_runtime(manifest) manifest["updated_at"] = utc_now() self.write(manifest) - def complete_run(self, *, index_size_bytes: int) -> dict[str, Any]: + def complete_run( + self, + *, + store_size_bytes_at_commit: int | None, + ) -> dict[str, Any]: manifest = self.read() + self._refresh_runtime(manifest) expected = set(manifest["inputs"]) completed = set(manifest["completed_videos"]) if expected != completed or manifest["failed_videos"]: manifest["state"] = "completed_with_failures" - manifest["index_size_bytes"] = index_size_bytes + manifest["store_size_bytes_at_commit"] = ( + store_size_bytes_at_commit + ) manifest["updated_at"] = utc_now() self.write(manifest) return manifest @@ -486,7 +551,7 @@ def complete_run(self, *, index_size_bytes: int) -> dict[str, Any]: manifest["state"] = "complete" manifest["completed_at"] = utc_now() manifest["processed_frames"] = processed_frames - manifest["index_size_bytes"] = index_size_bytes + manifest["store_size_bytes_at_commit"] = store_size_bytes_at_commit manifest["updated_at"] = utc_now() self.write(manifest) completion = { @@ -497,7 +562,31 @@ def complete_run(self, *, index_size_bytes: int) -> dict[str, Any]: "config_fingerprint": self.config.fingerprint(), "completed_at": manifest["completed_at"], "completed_videos": manifest["completed_videos"], - "index_size_bytes": index_size_bytes, + "store_size_bytes_at_commit": store_size_bytes_at_commit, } write_json_atomic(self.completion_path, completion) return manifest + + def record_storage_counts( + self, + record_counts: Mapping[str, int], + ) -> dict[str, Any]: + manifest = self.read() + if manifest.get("state") != "complete": + raise RuntimeError( + "Storage counts can only finalize a completed generation." + ) + counts = { + str(modality): int(count) + for modality, count in record_counts.items() + } + if set(counts) != set(self.config.enabled_modalities): + raise ValueError( + "Storage counts must cover every enabled modality exactly." + ) + if any(count < 0 for count in counts.values()): + raise ValueError("Storage record counts must be nonnegative.") + manifest["record_counts"] = counts + manifest["updated_at"] = utc_now() + self.write(manifest) + return manifest diff --git a/src/vidxp/core/media.py b/src/vidxp/core/media.py new file mode 100644 index 0000000..95b6baf --- /dev/null +++ b/src/vidxp/core/media.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +from datetime import datetime +from enum import StrEnum +from pathlib import Path +from typing import Literal + +from pydantic import ( + AwareDatetime, + BaseModel, + ConfigDict, + Field, + field_validator, + model_validator, +) + +from vidxp.core.identifiers import MediaId, MimeType, Sha256, VideoId +from vidxp.core.storage_keys import validate_storage_key + + +MEDIA_SCHEMA_VERSION = 1 + + +class InvalidMediaError(ValueError): + """Raised when media cannot be safely probed as a supported video.""" + + +class MediaProbeUnavailableError(RuntimeError): + """Raised when the configured media probe is unavailable.""" + + +class MediaStoreIntegrityError(RuntimeError): + """Raised when managed content no longer matches catalog authority.""" + + +class MediaImportLimitError(ValueError): + """Raised when a local import exceeds the configured byte limit.""" + + +class MediaUnavailableError(FileNotFoundError): + """Raised when a cataloged media asset cannot be materialized.""" + + +class MediaState(StrEnum): + ready = "ready" + + +class _MediaModel(BaseModel): + model_config = ConfigDict( + extra="forbid", + frozen=True, + allow_inf_nan=False, + strict=True, + ) + + +class MediaStream(_MediaModel): + index: int = Field(ge=0) + kind: str = Field(min_length=1) + codec: str = Field(min_length=1) + width: int | None = Field(default=None, gt=0) + height: int | None = Field(default=None, gt=0) + channels: int | None = Field(default=None, gt=0) + sample_rate: int | None = Field(default=None, gt=0) + + +class MediaProbe(_MediaModel): + detected_mime_type: MimeType + container: str = Field(min_length=1) + duration_seconds: float = Field(gt=0) + streams: tuple[MediaStream, ...] = Field(min_length=1) + + @property + def has_video(self) -> bool: + return any(stream.kind == "video" for stream in self.streams) + + @model_validator(mode="after") + def _require_video_stream(self) -> "MediaProbe": + if not self.has_video: + raise ValueError("media probe must contain a video stream") + return self + + +class StoredMedia(_MediaModel): + """Internal managed-media location produced by a media store.""" + + sha256: Sha256 + byte_size: int = Field(gt=0) + storage_key: str = Field(min_length=1) + local_path: Path + + @field_validator("storage_key") + @classmethod + def _validate_storage_key(cls, value: str) -> str: + return validate_storage_key(value) + + +class StagedMedia(_MediaModel): + """Quarantined media awaiting probe and managed-store publication.""" + + sha256: Sha256 + byte_size: int = Field(gt=0) + storage_key: str = Field(min_length=1) + path: Path + + @field_validator("storage_key") + @classmethod + def _validate_storage_key(cls, value: str) -> str: + return validate_storage_key(value) + + +class QuarantinedMedia(_MediaModel): + """Adapter-staged media accepted by the shared ingestion boundary.""" + + path: Path + original_filename: str = Field(min_length=1, max_length=255) + declared_mime_type: MimeType | None = None + + @field_validator("original_filename") + @classmethod + def _filename_only(cls, value: str) -> str: + return validate_display_filename(value) + + +class MediaRecord(_MediaModel): + """Authoritative internal catalog entry; storage_key is never projected.""" + + schema_version: Literal[MEDIA_SCHEMA_VERSION] = MEDIA_SCHEMA_VERSION + media_id: MediaId + video_id: VideoId + sha256: Sha256 + original_filename: str = Field(min_length=1, max_length=255) + byte_size: int = Field(gt=0) + declared_mime_type: MimeType | None = None + detected_mime_type: MimeType + container: str = Field(min_length=1) + duration_seconds: float = Field(gt=0) + streams: tuple[MediaStream, ...] = Field(min_length=1) + storage_key: str = Field(min_length=1) + state: MediaState = MediaState.ready + created_at: AwareDatetime + + @field_validator("original_filename") + @classmethod + def _filename_only(cls, value: str) -> str: + return validate_display_filename(value) + + @field_validator("storage_key") + @classmethod + def _validate_storage_key(cls, value: str) -> str: + return validate_storage_key(value) + + @model_validator(mode="after") + def _require_video_stream(self) -> "MediaRecord": + if not any(stream.kind == "video" for stream in self.streams): + raise ValueError("ready media must contain a video stream") + return self + + +def utc_now() -> datetime: + from datetime import timezone + + return datetime.now(timezone.utc) + + +def validate_display_filename(value: str) -> str: + if ( + Path(value).name != value + or "/" in value + or "\\" in value + or value in {".", ".."} + or any(ord(character) < 32 or ord(character) == 127 for character in value) + ): + raise ValueError( + "original_filename must be a basename without control characters" + ) + return value + + +def safe_media_suffix(path: Path) -> str: + """Return a bounded suffix suitable for temporary media staging.""" + + suffix = path.suffix.lower() + if ( + len(suffix) <= 10 + and suffix.startswith(".") + and suffix[1:].isalnum() + ): + return suffix + return ".bin" diff --git a/src/vidxp/core/runner.py b/src/vidxp/core/runner.py index 896652a..8c47099 100644 --- a/src/vidxp/core/runner.py +++ b/src/vidxp/core/runner.py @@ -1,22 +1,17 @@ from __future__ import annotations from pathlib import Path -from threading import Lock from time import perf_counter from typing import Any, Callable, Sequence from filelock import FileLock, Timeout -from vidxp.capabilities.registry import ( - get_capability, - require_dependencies, -) +from vidxp.capabilities.registry import CapabilityRegistry from vidxp.core.contracts import ( INDEX_SCHEMA_VERSION, CancellationToken, IndexCancelledError, IndexConfig, - IndexSchemaError, VideoSource, ) from vidxp.core.manifest import ( @@ -25,15 +20,22 @@ source_checksum, source_checksums, ) -from vidxp.core.storage import IndexStorage -from vidxp.index_state import ( - IndexingInProgressError, - write_index_status, -) +from vidxp.index_state import IndexingInProgressError +from vidxp.ports import IndexStore, ModelRuntimePort ProgressCallback = Callable[[dict[str, Any]], None] -_INDEXING_LOCK = Lock() + + +def require_dependencies( + names: tuple[str, ...], + *, + source: VideoSource, + registry: CapabilityRegistry, +) -> None: + """Single testable dependency gate for an injected registry.""" + + registry.require_dependencies(names, source=source) class _RunLock: @@ -68,47 +70,8 @@ def _run_lock_held(run_directory: Path) -> bool: return False -def indexing_in_progress(config: IndexConfig | None = None) -> bool: - if _INDEXING_LOCK.locked(): - return True - active_config = config or IndexConfig.local() - return _run_lock_held(active_config.run_directory) - - -def local_config_from_status( - status: dict[str, Any], - *, - storage_directory: str | Path | None = None, -) -> IndexConfig: - summary = status.get("summary") or {} - if summary.get("index_schema_version") != INDEX_SCHEMA_VERSION: - raise IndexSchemaError( - "The saved index predates the benchmark-ready schema. " - "Re-index the video before searching." - ) - stored = dict(summary.get("configuration") or {}) - stored = { - key: value - for key, value in stored.items() - if key in IndexConfig.__dataclass_fields__ - } - stored.update( - { - "dataset": str(summary["dataset"]), - "split": str(summary["split"]), - "run_id": str(summary["run_id"]), - "video_id": str(summary["video_id"]), - } - ) - if storage_directory is not None: - stored["storage_directory"] = str(storage_directory) - else: - stored.setdefault("storage_directory", "chroma_data") - if "enabled_modalities" in stored: - stored["enabled_modalities"] = tuple(stored["enabled_modalities"]) - if "collection_names" in stored: - stored["collection_names"] = dict(stored["collection_names"]) - return IndexConfig(**stored) +def indexing_in_progress(config: IndexConfig) -> bool: + return _run_lock_held(config.run_directory) def _resolve_sources( @@ -140,19 +103,22 @@ def _run_capability_group( names: tuple[str, ...], source: VideoSource, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, manifest: ManifestStore, cancellation: CancellationToken, progress_callback: ProgressCallback | None, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, ) -> dict[str, Any]: - definitions = tuple(get_capability(name) for name in names) - indexer = definitions[0].indexer + definitions = tuple(registry.get(name) for name in names) + indexers = tuple(registry.executor(name).indexer for name in names) + indexer = indexers[0] index_stage = definitions[0].index_stage if indexer is None or index_stage is None: raise ValueError( f"Capability {names[0]!r} does not support indexing." ) - if any(definition.indexer is not indexer for definition in definitions): + if any(candidate is not indexer for candidate in indexers): raise RuntimeError("Grouped capabilities must share one indexer.") if any( definition.index_stage != index_stage @@ -192,6 +158,8 @@ def stage_progress(event: dict[str, Any]) -> None: cancellation=cancellation, progress=stage_progress, modalities=names, + registry=registry, + runtime=runtime, ) except BaseException: if active_substage is not None and active_substage != index_stage: @@ -241,23 +209,27 @@ def stage_progress(event: dict[str, Any]) -> None: return dict(result.summary) -def _index_groups(names: tuple[str, ...]) -> tuple[tuple[str, ...], ...]: +def _index_groups( + names: tuple[str, ...], + registry: CapabilityRegistry, +) -> tuple[tuple[str, ...], ...]: groups: list[list[str]] = [] - handlers = [] + execution_groups: list[str] = [] for name in names: - handler = get_capability(name).indexer - if handler is None: + definition = registry.get(name) + if registry.executor(name).indexer is None: raise ValueError( f"Capability {name!r} does not support indexing." ) - try: - group_index = next( - index - for index, existing in enumerate(handlers) - if existing is handler + execution_group = definition.execution_group + if execution_group is None: + raise RuntimeError( + f"Capability {name!r} has no execution group." ) - except StopIteration: - handlers.append(handler) + try: + group_index = execution_groups.index(execution_group) + except ValueError: + execution_groups.append(execution_group) groups.append([name]) else: groups[group_index].append(name) @@ -267,16 +239,18 @@ def _index_groups(names: tuple[str, ...]) -> tuple[tuple[str, ...], ...]: def _run_enabled_modalities( source: VideoSource, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, manifest: ManifestStore, cancellation: CancellationToken, progress_callback: ProgressCallback | None, set_stage: Callable[[str], None], + registry: CapabilityRegistry, + runtime: ModelRuntimePort, ) -> dict[str, Any]: summary: dict[str, Any] = {} - for names in _index_groups(config.enabled_modalities): + for names in _index_groups(config.enabled_modalities, registry): cancellation.raise_if_cancelled() - set_stage(get_capability(names[0]).index_stage) + set_stage(str(registry.get(names[0]).index_stage)) summary.update( _run_capability_group( names, @@ -286,6 +260,8 @@ def _run_enabled_modalities( manifest, cancellation, progress_callback, + registry, + runtime, ) ) return summary @@ -296,12 +272,14 @@ def _process_video( source: VideoSource, checksum: str, config: IndexConfig, - storage: IndexStorage, + storage: IndexStore, manifest: ManifestStore, cancellation: CancellationToken, progress_callback: ProgressCallback | None, *, fail_fast: bool, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, ) -> None: video_config = config.for_video(video_id) manifest.start_video(video_id) @@ -320,10 +298,19 @@ def set_stage(value: str) -> None: require_dependencies( config.enabled_modalities, source=source, + registry=registry, ) stage = "preparing_storage" - for modality in config.enabled_modalities: - storage.delete_video(modality, video_id) + if config.generation_id is not None: + for modality in config.enabled_modalities: + storage.delete_records( + modality, + video_id=video_id, + filters={"generation_id": config.generation_id}, + ) + else: + for modality in config.enabled_modalities: + storage.delete_video(modality, video_id) summary.update( _run_enabled_modalities( @@ -334,6 +321,8 @@ def set_stage(value: str) -> None: cancellation, progress_callback, set_stage, + registry, + runtime, ) ) manifest.complete_video( @@ -380,54 +369,53 @@ def _run_index_unlocked( resume: bool = True, reset: bool = False, fail_fast: bool = True, - storage: IndexStorage | None = None, - manifest_store: ManifestStore | None = None, + storage: IndexStore, + manifest_store: ManifestStore, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, ) -> dict[str, Any]: if not sources: raise ValueError("At least one video or transcript source is required.") cancellation = cancellation or CancellationToken() resolved = _resolve_sources(sources, config) - owns_storage = storage is None - store = storage or IndexStorage(config) - try: - manifest = manifest_store or ManifestStore(config) - if reset: - store.clear() - manifest.initialize(resolved, reset=reset) - - for video_id, source, checksum, _ in resolved: - if resume and manifest.completed( - video_id, - checksum=checksum, - config_fingerprint=config.fingerprint(), - ): - _report( - progress_callback, - { - "state": "skipped", - "stage": "checkpoint", - "message": f"Skipping completed video {video_id}.", - "video_id": video_id, - }, - ) - continue + if reset: + storage.clear() + manifest_store.initialize(resolved, reset=reset) - _process_video( - video_id, - source, - checksum, - config, - store, - manifest, - cancellation, + for video_id, source, checksum, _ in resolved: + if resume and manifest_store.completed( + video_id, + checksum=checksum, + config_fingerprint=config.fingerprint(), + ): + _report( progress_callback, - fail_fast=fail_fast, + { + "state": "skipped", + "stage": "checkpoint", + "message": f"Skipping completed video {video_id}.", + "video_id": video_id, + }, ) + continue - return manifest.complete_run(index_size_bytes=store.size_bytes()) - finally: - if owns_storage: - store.close() + _process_video( + video_id, + source, + checksum, + config, + storage, + manifest_store, + cancellation, + progress_callback, + fail_fast=fail_fast, + registry=registry, + runtime=runtime, + ) + + return manifest_store.complete_run( + store_size_bytes_at_commit=storage.size_bytes() + ) def run_index( @@ -439,8 +427,10 @@ def run_index( resume: bool = True, reset: bool = False, fail_fast: bool = True, - storage: IndexStorage | None = None, - manifest_store: ManifestStore | None = None, + storage: IndexStore, + manifest_store: ManifestStore, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, ) -> dict[str, Any]: with _RunLock(config.run_directory): return _run_index_unlocked( @@ -453,36 +443,32 @@ def run_index( fail_fast=fail_fast, storage=storage, manifest_store=manifest_store, + registry=registry, + runtime=runtime, ) -def _video_status(path: Path, source_name: str, checksum: str) -> dict[str, Any]: - stat = path.stat() - return { - "path": str(path.resolve()), - "source_name": source_name, - "size": stat.st_size, - "sha256": checksum, - } - - def index_video( path: str, progress_callback: ProgressCallback | None = None, source_name: str | None = None, + checksum: str | None = None, *, - config: IndexConfig | None = None, + config: IndexConfig, cancellation: CancellationToken | None = None, + storage: IndexStore, + manifest_store: ManifestStore, + registry: CapabilityRegistry, + runtime: ModelRuntimePort, ) -> dict[str, Any]: input_path = Path(path) if not input_path.is_file(): raise FileNotFoundError(f"Video not found: {input_path}") - if not _INDEXING_LOCK.acquire(blocking=False): - raise IndexingInProgressError("Another video is already being indexed.") source = VideoSource( path=input_path, source_name=source_name or input_path.name, + checksum=checksum, ) checksum = source_checksum(source) source = VideoSource( @@ -490,13 +476,8 @@ def index_video( source_name=source.source_name, checksum=checksum, ) - active_config = config or IndexConfig.local(video_id=checksum) + active_config = config video_id = active_config.video_id or checksum - video = _video_status( - input_path, - source.source_name or input_path.name, - checksum, - ) latest_event: dict[str, Any] = { "state": "indexing", "stage": "initializing", @@ -504,17 +485,6 @@ def index_video( def report(event: dict[str, Any]) -> None: latest_event.update(event) - write_index_status( - state=event["state"], - stage=event["stage"], - message=event["message"], - video=video, - current=event.get("current"), - total=event.get("total"), - summary=event.get("summary"), - error=event.get("error"), - index_directory=active_config.index_directory, - ) if progress_callback is not None: progress_callback(event) @@ -522,10 +492,7 @@ def report(event: dict[str, Any]) -> None: { "state": "indexing", "stage": "initializing", - "message": ( - "Preparing the selected indexing modalities. " - "Missing model weights will download before their first use." - ), + "message": "Preparing the selected indexing modalities.", } ) try: @@ -534,8 +501,12 @@ def report(event: dict[str, Any]) -> None: active_config, progress_callback=report, cancellation=cancellation, + storage=storage, + manifest_store=manifest_store, + registry=registry, + runtime=runtime, resume=False, - reset=True, + reset=False, fail_fast=True, ) summary = dict(manifest["videos"][video_id]["summary"]) @@ -546,7 +517,6 @@ def report(event: dict[str, Any]) -> None: "split": active_config.split, "run_id": active_config.run_id, "video_id": video_id, - "configuration": active_config.to_dict(), } ) report( @@ -579,5 +549,3 @@ def report(event: dict[str, Any]) -> None: } ) raise - finally: - _INDEXING_LOCK.release() diff --git a/src/vidxp/core/snapshots.py b/src/vidxp/core/snapshots.py new file mode 100644 index 0000000..b03c2fe --- /dev/null +++ b/src/vidxp/core/snapshots.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import ( + AwareDatetime, + BaseModel, + ConfigDict, + Field, + JsonValue, + model_validator, +) + +from vidxp.core.identifiers import ( + Identifier, + IndexGenerationId, + IndexSnapshotId, + Sha256, +) + + +INDEX_SNAPSHOT_SCHEMA_VERSION = 1 +ACTIVE_SNAPSHOT_POINTER_SCHEMA_VERSION = 1 + +class _SnapshotModel(BaseModel): + model_config = ConfigDict( + allow_inf_nan=False, + extra="forbid", + frozen=True, + strict=True, + ) + + +class GenerationReference(_SnapshotModel): + generation_id: IndexGenerationId + media_id: Identifier + manifest_sha256: Sha256 + input_sha256: Sha256 + config_fingerprint: Sha256 + modalities: tuple[Identifier, ...] = Field(min_length=1) + record_counts: dict[Identifier, int] + store_size_bytes_at_commit: int | None = Field(ge=0) + + @model_validator(mode="after") + def _validate_record_counts(self) -> GenerationReference: + if set(self.record_counts) != set(self.modalities): + raise ValueError( + "record_counts keys must exactly match modalities" + ) + if any(count < 0 for count in self.record_counts.values()): + raise ValueError("record_counts values must be nonnegative") + return self + + +class IndexSnapshot(_SnapshotModel): + schema_version: Literal[INDEX_SNAPSHOT_SCHEMA_VERSION] = ( + INDEX_SNAPSHOT_SCHEMA_VERSION + ) + snapshot_id: IndexSnapshotId + created_at: AwareDatetime + config_fingerprint: Sha256 + configuration: dict[str, JsonValue] + generations: dict[Identifier, GenerationReference] + + @model_validator(mode="after") + def _validate_generation_keys(self) -> IndexSnapshot: + mismatched = sorted( + media_id + for media_id, reference in self.generations.items() + if media_id != reference.media_id + ) + if mismatched: + raise ValueError( + "generation mapping keys must match reference media_id values" + ) + return self + + +class ActiveSnapshotPointer(_SnapshotModel): + schema_version: Literal[ACTIVE_SNAPSHOT_POINTER_SCHEMA_VERSION] = ( + ACTIVE_SNAPSHOT_POINTER_SCHEMA_VERSION + ) + snapshot_id: IndexSnapshotId + snapshot_sha256: Sha256 + updated_at: AwareDatetime diff --git a/src/vidxp/core/storage.py b/src/vidxp/core/storage.py index 50ac3db..6dfd9d5 100644 --- a/src/vidxp/core/storage.py +++ b/src/vidxp/core/storage.py @@ -1,7 +1,8 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Iterable, Mapping +from typing import Any, Callable, Iterable, Mapping, TypeVar +from urllib.parse import urlsplit from vidxp.core.contracts import ( CancellationToken, @@ -10,11 +11,39 @@ batched, ) +_T = TypeVar("_T") + +BUNDLED_CHROMA_SERVER_URL = "http://chroma:8000" + + +class IndexStorageUnavailableError(RuntimeError): + """The configured remote vector store could not serve an operation.""" + + +def _is_remote_unavailable(exc: Exception) -> bool: + """Return whether an exception represents a retryable remote failure.""" + + import httpx + from chromadb.errors import ( + InternalError, + RateLimitError, + ) + + return isinstance( + exc, + ( + httpx.TransportError, + InternalError, + RateLimitError, + ), + ) + def metadata_filter( config: IndexConfig, *, video_id: str | None = None, + generation_ids: Iterable[str] | None = None, extra: Mapping[str, Any] | None = None, ) -> dict[str, Any]: values: dict[str, Any] = { @@ -24,6 +53,11 @@ def metadata_filter( } if video_id is not None: values["video_id"] = video_id + selected_generations = _generation_ids(generation_ids) + if selected_generations is not None: + if not selected_generations: + raise ValueError("generation_ids must not be empty.") + values["generation_id"] = {"$in": list(selected_generations)} if extra: reserved = set(values) & set(extra) if reserved: @@ -36,20 +70,91 @@ def metadata_filter( return clauses[0] if len(clauses) == 1 else {"$and": clauses} -def _client_for_path(path: str): - import chromadb +def _generation_ids( + generation_ids: Iterable[str] | None, +) -> tuple[str, ...] | None: + if generation_ids is None: + return None + if isinstance(generation_ids, str): + raise TypeError("generation_ids must be an iterable of identifiers.") + values = tuple(generation_ids) + if any(not isinstance(value, str) or not value for value in values): + raise ValueError("generation_ids must contain non-empty strings.") + return tuple(dict.fromkeys(values)) + - return chromadb.PersistentClient(path=path) +class ChromaClientFactory: + def __init__(self, server_url: str | None = None) -> None: + self.server_url = server_url + + @property + def remote(self) -> bool: + return self.server_url is not None + + def create(self, path: Path): + import chromadb + + if self.server_url is None: + return chromadb.PersistentClient(path=str(path.resolve())) + parsed = urlsplit(self.server_url) + return chromadb.HttpClient( + host=parsed.hostname or "", + port=parsed.port or (443 if parsed.scheme == "https" else 80), + ssl=parsed.scheme == "https", + ) + + def heartbeat(self) -> int: + client = self.create(Path(".")) + try: + return int(client.heartbeat()) + finally: + close = getattr(client, "close", None) + if close is not None: + close() class IndexStorage: - def __init__(self, config: IndexConfig): + def __init__( + self, + config: IndexConfig, + *, + create: bool = True, + client_factory: ChromaClientFactory | None = None, + ): self.config = config self.path = config.index_directory - self.path.mkdir(parents=True, exist_ok=True) - self.client = _client_for_path(str(self.path.resolve())) + self._create = create + self._client_factory = client_factory or ChromaClientFactory() self._names = dict(config.collection_names) self._collections: dict[str, Any] = {} + if create and not self._client_factory.remote: + self.path.mkdir(parents=True, exist_ok=True) + elif not self._client_factory.remote and not self.path.is_dir(): + raise FileNotFoundError( + f"The committed Chroma store is missing at {self.path}." + ) + try: + self.client = self._client_factory.create(self.path) + except Exception as exc: + if ( + self._client_factory.remote + and _is_remote_unavailable(exc) + ): + raise IndexStorageUnavailableError( + "The remote Chroma service is unavailable." + ) from exc + raise + if not create: + try: + existing = self._call(self.client.list_collections) + except Exception: + self.close() + raise + if not existing: + self.close() + raise FileNotFoundError( + f"The committed Chroma store is missing at {self.path}." + ) def close(self) -> None: self._collections.clear() @@ -70,10 +175,24 @@ def collection(self, modality: str): name = self._names[modality] except KeyError as exc: raise ValueError(f"Unsupported collection modality: {modality}") from exc - collection = self.client.get_or_create_collection( - name=name, - metadata={"hnsw:space": self.config.vector_distance}, - ) + if self._create: + collection = self._call( + self.client.get_or_create_collection, + name=name, + metadata={"hnsw:space": self.config.vector_distance}, + ) + else: + from chromadb.errors import NotFoundError + + try: + collection = self._call( + self.client.get_collection, + name=name, + ) + except NotFoundError as exc: + raise FileNotFoundError( + f"Committed Chroma collection {name!r} is missing." + ) from exc configuration = getattr(collection, "configuration", {}) or {} actual_distance = (configuration.get("hnsw") or {}).get("space") if ( @@ -87,20 +206,34 @@ def collection(self, modality: str): self._collections[modality] = collection return collection + def _call(self, operation: Callable[..., _T], /, *args, **kwargs) -> _T: + try: + return operation(*args, **kwargs) + except Exception as exc: + if ( + self._client_factory.remote + and _is_remote_unavailable(exc) + ): + raise IndexStorageUnavailableError( + "The remote Chroma service is unavailable." + ) from exc + raise + def clear(self, modalities: Iterable[str] | None = None) -> None: selected = tuple(modalities or self._names) existing = { getattr(collection, "name", collection) - for collection in self.client.list_collections() + for collection in self._call(self.client.list_collections) } for modality in selected: name = self._names[modality] if name in existing: - self.client.delete_collection(name) + self._call(self.client.delete_collection, name) self._collections.pop(modality, None) def delete_video(self, modality: str, video_id: str) -> None: - self.collection(modality).delete( + self._call( + self.collection(modality).delete, where=metadata_filter(self.config, video_id=video_id), ) @@ -111,7 +244,8 @@ def delete_records( video_id: str, filters: Mapping[str, Any] | None = None, ) -> None: - self.collection(modality).delete( + self._call( + self.collection(modality).delete, where=metadata_filter( self.config, video_id=video_id, @@ -119,6 +253,23 @@ def delete_records( ), ) + def delete_generation( + self, + generation_id: str, + modalities: Iterable[str] | None = None, + ) -> None: + selected = ( + tuple(self._names) + if modalities is None + else tuple(modalities) + ) + where = metadata_filter( + self.config, + generation_ids=(generation_id,), + ) + for modality in selected: + self._call(self.collection(modality).delete, where=where) + def upsert( self, modality: str, @@ -142,7 +293,7 @@ def upsert( } if any(record.document is not None for record in group): options["documents"] = [record.document or "" for record in group] - collection.upsert(**options) + self._call(collection.upsert, **options) stored += len(group) return stored @@ -153,10 +304,14 @@ def query( *, top_k: int, video_id: str | None = None, + generation_ids: Iterable[str] | None = None, filters: Mapping[str, Any] | None = None, ) -> list[dict[str, Any]]: if top_k <= 0: raise ValueError("top_k must be greater than zero.") + selected_generations = _generation_ids(generation_ids) + if selected_generations == (): + return [] options: dict[str, Any] = { "query_embeddings": [embedding], @@ -165,10 +320,11 @@ def query( "where": metadata_filter( self.config, video_id=video_id, + generation_ids=selected_generations, extra=filters, ), } - result = self.collection(modality).query(**options) + result = self._call(self.collection(modality).query, **options) ids = (result.get("ids") or [[]])[0] metadatas = (result.get("metadatas") or [[]])[0] distances = (result.get("distances") or [[]])[0] @@ -190,15 +346,29 @@ def records( modality: str, *, video_id: str | None = None, + generation_ids: Iterable[str] | None = None, filters: Mapping[str, Any] | None = None, + limit: int | None = None, + offset: int = 0, ) -> list[dict[str, Any]]: - result = self.collection(modality).get( + selected_generations = _generation_ids(generation_ids) + if selected_generations == (): + return [] + if limit is not None and limit <= 0: + raise ValueError("limit must be positive") + if offset < 0: + raise ValueError("offset must be nonnegative") + result = self._call( + self.collection(modality).get, where=metadata_filter( self.config, video_id=video_id, + generation_ids=selected_generations, extra=filters, ), include=["metadatas"], + limit=limit, + offset=offset, ) return [ dict(metadata) @@ -206,10 +376,111 @@ def records( if metadata ] - def size_bytes(self) -> int: + def count_records( + self, + modality: str, + *, + video_id: str | None = None, + generation_ids: Iterable[str] | None = None, + filters: Mapping[str, Any] | None = None, + ) -> int: + selected_generations = _generation_ids(generation_ids) + if selected_generations == (): + return 0 + result = self._call( + self.collection(modality).get, + where=metadata_filter( + self.config, + video_id=video_id, + generation_ids=selected_generations, + extra=filters, + ), + include=[], + ) + return len(result.get("ids") or ()) + + def size_bytes(self) -> int | None: + if self._client_factory.remote: + return None return directory_size(self.path) +class SnapshotScopedIndexStore: + """Read-only view of records referenced by one immutable snapshot.""" + + def __init__( + self, + store: IndexStorage, + generation_ids: Iterable[str], + ) -> None: + self._store = store + self._generation_ids = _generation_ids(generation_ids) or () + + def close(self) -> None: + self._store.close() + + def __enter__(self) -> "SnapshotScopedIndexStore": + self._store.__enter__() + return self + + def __exit__(self, *args: Any) -> Any: + return self._store.__exit__(*args) + + def size_bytes(self) -> int | None: + return self._store.size_bytes() + + def query( + self, + modality: str, + embedding: list[float], + *, + top_k: int, + video_id: str | None = None, + filters: Mapping[str, Any] | None = None, + ) -> list[dict[str, Any]]: + return self._store.query( + modality, + embedding, + top_k=top_k, + video_id=video_id, + generation_ids=self._generation_ids, + filters=filters, + ) + + def records( + self, + modality: str, + *, + video_id: str | None = None, + filters: Mapping[str, Any] | None = None, + limit: int | None = None, + offset: int = 0, + ) -> list[dict[str, Any]]: + options: dict[str, Any] = { + "video_id": video_id, + "generation_ids": self._generation_ids, + "filters": filters, + } + if limit is not None: + options["limit"] = limit + if offset: + options["offset"] = offset + return self._store.records(modality, **options) + + def count_records( + self, + modality: str, + *, + video_id: str | None = None, + filters: Mapping[str, Any] | None = None, + ) -> int: + return self._store.count_records( + modality, + video_id=video_id, + generation_ids=self._generation_ids, + filters=filters, + ) + def directory_size(path: str | Path) -> int: root = Path(path) if not root.exists(): diff --git a/src/vidxp/core/storage_keys.py b/src/vidxp/core/storage_keys.py new file mode 100644 index 0000000..f1fc183 --- /dev/null +++ b/src/vidxp/core/storage_keys.py @@ -0,0 +1,13 @@ +from pathlib import PurePosixPath + + +def validate_storage_key(value: str) -> str: + key = PurePosixPath(value) + if ( + key.is_absolute() + or ".." in key.parts + or "\\" in value + or value != key.as_posix() + ): + raise ValueError("storage_key must be a normalized relative key") + return value diff --git a/src/vidxp/core/uploads.py b/src/vidxp/core/uploads.py new file mode 100644 index 0000000..d373a2b --- /dev/null +++ b/src/vidxp/core/uploads.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from enum import StrEnum + +from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator + +from vidxp.core.identifiers import ( + JobId, + MediaId, + MimeType, + Uuid4Hex, +) +from vidxp.core.media import validate_display_filename + + +class UploadState(StrEnum): + pending = "pending" + accepted = "accepted" + processing = "processing" + ready = "ready" + failed = "failed" + expired = "expired" + + +class UploadIntentRecord(BaseModel): + """Authoritative upload state; tus upload identity stays internal.""" + + model_config = ConfigDict( + extra="forbid", + frozen=True, + allow_inf_nan=False, + ) + + intent_id: Uuid4Hex + request_key: str = Field(pattern=r"^[0-9a-f]{64}$") + original_filename: str = Field(min_length=1, max_length=255) + byte_size: int = Field(gt=0) + declared_mime_type: MimeType | None = None + state: UploadState + created_at: AwareDatetime + expires_at: AwareDatetime + upload_id: str | None = Field( + default=None, + min_length=1, + max_length=255, + pattern=r"^[A-Za-z0-9][A-Za-z0-9._~-]*$", + ) + job_id: JobId | None = None + media_id: MediaId | None = None + + @model_validator(mode="after") + def _validate_state(self) -> "UploadIntentRecord": + validate_display_filename(self.original_filename) + if self.expires_at <= self.created_at: + raise ValueError("upload expiry must follow creation") + if self.state == UploadState.pending and self.upload_id is not None: + raise ValueError("pending uploads cannot have an upload identifier") + if self.state in { + UploadState.accepted, + UploadState.processing, + UploadState.failed, + } and self.upload_id is None: + raise ValueError(f"{self.state} uploads require an upload identifier") + if self.state in { + UploadState.processing, + UploadState.failed, + UploadState.ready, + } and self.job_id is None: + raise ValueError(f"{self.state} uploads require a job identifier") + if self.state in { + UploadState.pending, + UploadState.accepted, + } and self.job_id is not None: + raise ValueError(f"{self.state} uploads cannot have a job identifier") + if self.state == UploadState.ready and self.media_id is None: + raise ValueError("ready uploads require a media identifier") + if self.media_id is not None and self.state != UploadState.ready: + raise ValueError("only ready uploads may reference media") + return self diff --git a/src/vidxp/core/video.py b/src/vidxp/core/video.py index 0f6e3b7..538cba4 100644 --- a/src/vidxp/core/video.py +++ b/src/vidxp/core/video.py @@ -1,11 +1,12 @@ from __future__ import annotations from dataclasses import dataclass +from math import floor from pathlib import Path -from shutil import which -from typing import Iterator +from typing import Iterator, Sequence from vidxp.core.contracts import CancellationToken +from vidxp.core.indexing_common import ProgressCallback @dataclass(frozen=True) @@ -30,19 +31,57 @@ class FrameSample: frame: object -def ffmpeg_binary() -> str: - from moviepy.config import get_setting +@dataclass(frozen=True) +class FrameSampling: + frame_stride: int | None = None + source_fps: float | None = None + target_fps: float | None = None - configured = get_setting("FFMPEG_BINARY") - configured_path = Path(str(configured)) - resolved = ( - str(configured_path.resolve()) - if configured_path.is_file() - else which(str(configured)) - ) - if not resolved: - raise RuntimeError(f"FFmpeg executable was not found: {configured}") - return resolved + def __post_init__(self) -> None: + frame_based = self.frame_stride is not None + time_based = self.source_fps is not None or self.target_fps is not None + if frame_based == time_based: + raise ValueError( + "Frame sampling requires either a stride or a source/target FPS pair." + ) + if frame_based and self.frame_stride <= 0: + raise ValueError("frame_stride must be greater than zero.") + if time_based and ( + self.source_fps is None + or self.target_fps is None + or self.source_fps <= 0 + or self.target_fps <= 0 + ): + raise ValueError("source_fps and target_fps must be greater than zero.") + + def includes(self, frame_index: int, timestamp: float) -> bool: + if self.frame_stride is not None: + return frame_index % self.frame_stride == 0 + assert self.source_fps is not None + assert self.target_fps is not None + if self.source_fps <= self.target_fps or frame_index == 0: + return True + previous_timestamp = (frame_index - 1) / self.source_fps + return floor(timestamp * self.target_fps) > floor( + previous_timestamp * self.target_fps + ) + + def next_sample_timestamp( + self, + frame_index: int, + *, + frame_count: int, + duration: float, + ) -> float: + if self.source_fps is None: + raise ValueError( + "Next sample timestamps require time-based sampling." + ) + for next_index in range(frame_index + 1, max(0, frame_count)): + timestamp = next_index / self.source_fps + if self.includes(next_index, timestamp): + return min(duration, timestamp) + return duration def probe_video(path: str | Path) -> VideoInfo: @@ -68,15 +107,20 @@ def probe_video(path: str | Path) -> VideoInfo: def iter_frame_batches( path: str | Path, *, - frame_stride: int, + frame_stride: int | None = None, + frame_strides: Sequence[int] | None = None, + samplings: Sequence[FrameSampling] | None = None, batch_size: int, cancellation: CancellationToken, stats: FrameStreamStats | None = None, ) -> Iterator[list[FrameSample]]: import cv2 - if frame_stride <= 0: - raise ValueError("frame_stride must be greater than zero.") + strides = tuple(dict.fromkeys(frame_strides or ())) + if frame_stride is not None: + strides = tuple(dict.fromkeys((frame_stride, *strides))) + if any(stride <= 0 for stride in strides): + raise ValueError("frame strides must be greater than zero.") if batch_size <= 0: raise ValueError("batch_size must be greater than zero.") @@ -85,13 +129,23 @@ def iter_frame_batches( if fps <= 0: video.release() raise ValueError("The selected video has an invalid frame rate.") + active_samplings = tuple(samplings or ()) + tuple( + FrameSampling(frame_stride=stride) for stride in strides + ) + if not active_samplings: + video.release() + raise ValueError("At least one frame sampling schedule is required.") stream_stats = stats or FrameStreamStats() batch: list[FrameSample] = [] frame_index = 0 try: while True: - sampled = frame_index % frame_stride == 0 + timestamp = frame_index / fps + sampled = any( + sampling.includes(frame_index, timestamp) + for sampling in active_samplings + ) if sampled: retrieved, frame = video.read() else: @@ -105,7 +159,7 @@ def iter_frame_batches( batch.append( FrameSample( frame_index=frame_index, - timestamp=frame_index / fps, + timestamp=timestamp, frame=frame, ) ) @@ -121,26 +175,18 @@ def iter_frame_batches( video.release() -def extract_audio(input_path: str | Path, output_path: str | Path) -> Path: - from moviepy.editor import VideoFileClip - - destination = Path(output_path) - destination.parent.mkdir(parents=True, exist_ok=True) - with VideoFileClip(str(input_path)) as source_video: - if source_video.audio is None: - raise ValueError("The selected video does not contain an audio track.") - source_video.audio.write_audiofile(str(destination), logger=None) - return destination - - def render_actor_video( input_path: str | Path, output_path: str | Path, cluster_id: str, detections: list[dict], + *, + cancellation: CancellationToken | None = None, + progress: ProgressCallback | None = None, ) -> None: import cv2 + active_cancellation = cancellation or CancellationToken() source_path = Path(input_path) destination = Path(output_path) if not source_path.is_file(): @@ -157,6 +203,7 @@ def render_actor_video( fps = float(source.get(cv2.CAP_PROP_FPS)) width = int(source.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(source.get(cv2.CAP_PROP_FRAME_HEIGHT)) + frame_count = int(source.get(cv2.CAP_PROP_FRAME_COUNT)) if fps <= 0 or width <= 0 or height <= 0: source.release() raise RuntimeError( @@ -188,6 +235,7 @@ def render_actor_video( try: frame_index = 0 while True: + active_cancellation.raise_if_cancelled() retrieved, frame = source.read() if not retrieved: break @@ -215,10 +263,32 @@ def render_actor_video( writer.write(frame) frame_index += 1 frames_written += 1 + if progress is not None and frames_written % 30 == 0: + progress( + { + "state": "rendering", + "stage": "rendering", + "message": "Rendering the actor overlay.", + "current": frames_written, + "total": frame_count if frame_count > 0 else None, + } + ) finally: source.release() writer.release() + active_cancellation.raise_if_cancelled() + if progress is not None and frames_written: + progress( + { + "state": "rendering", + "stage": "rendering", + "message": "Rendered the actor overlay.", + "current": frames_written, + "total": frame_count if frame_count > 0 else frames_written, + } + ) + if ( frames_written == 0 or not destination.is_file() diff --git a/src/vidxp/execution.py b/src/vidxp/execution.py new file mode 100644 index 0000000..d1e94fc --- /dev/null +++ b/src/vidxp/execution.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import hashlib +from dataclasses import dataclass, field +from typing import Any +from uuid import UUID + +from vidxp.core.contracts import CancellationToken +from vidxp.core.indexing_common import ProgressCallback + + +@dataclass(frozen=True) +class ExecutionContext: + """Runtime-only progress and cancellation controls for worker operations.""" + + job_id: str | None = None + progress: ProgressCallback | None = None + cancellation: CancellationToken = field(default_factory=CancellationToken) + + def report(self, event: dict[str, Any]) -> None: + self.cancellation.raise_if_cancelled() + if self.progress is not None: + self.progress(event) + + def checkpoint(self) -> None: + self.cancellation.raise_if_cancelled() + + @property + def operation_id(self) -> str | None: + """Return a filesystem-safe UUID identity without changing the job ID.""" + + if self.job_id is None: + return None + identifier = UUID(self.job_id) + if identifier.version == 4: + return identifier.hex + digest = hashlib.sha256(self.job_id.encode()).digest()[:16] + return UUID(bytes=digest, version=4).hex + + +def execution_context(value: ExecutionContext | None) -> ExecutionContext: + return value if value is not None else ExecutionContext() diff --git a/src/vidxp/idempotency.py b/src/vidxp/idempotency.py new file mode 100644 index 0000000..a54c1ae --- /dev/null +++ b/src/vidxp/idempotency.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from hashlib import sha256 +from typing import Annotated, Literal, TypeAlias +from uuid import UUID + +from pydantic import StringConstraints + +from vidxp.application_models import Principal + + +IdempotencyKey: TypeAlias = Annotated[ + str, + StringConstraints( + min_length=8, + max_length=200, + pattern=r"^[\x21-\x7e]+$", + ), +] +RequestTransport: TypeAlias = Literal["http", "mcp"] +_SINGLE_REPOSITORY_SCOPE = "default" + + +def scoped_request_key( + *, + principal: Principal, + transport: RequestTransport, + operation: str, + idempotency_key: str, +) -> str: + """Derive a non-reversible request identity within one adapter namespace.""" + + material = "\0".join( + ( + f"vidxp-{transport}-request-v1", + _SINGLE_REPOSITORY_SCOPE, + principal.subject, + operation, + idempotency_key, + ) + ).encode() + return sha256(material).hexdigest() + + +def scoped_job_id( + *, + principal: Principal, + transport: RequestTransport, + operation: str, + idempotency_key: str, +) -> str: + """Project a scoped request identity into a valid deterministic job UUID.""" + + digest = scoped_request_key( + principal=principal, + transport=transport, + operation=operation, + idempotency_key=idempotency_key, + ) + value = bytearray(bytes.fromhex(digest)[:16]) + value[6] = (value[6] & 0x0F) | 0x40 + value[8] = (value[8] & 0x3F) | 0x80 + return UUID(bytes=bytes(value)).hex diff --git a/src/vidxp/index_state.py b/src/vidxp/index_state.py index c3e4b74..3fcd530 100644 --- a/src/vidxp/index_state.py +++ b/src/vidxp/index_state.py @@ -1,107 +1,51 @@ -import hashlib -import json -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - - -INDEX_DIRECTORY = Path("chroma_data") -INDEX_STATUS_FILE = "index_status.json" -INDEX_STATUS_SCHEMA = 1 - - -class IndexNotReadyError(RuntimeError): - """Raised when a search is attempted without a completed index.""" +from __future__ import annotations +from typing import Any -class IndexingInProgressError(RuntimeError): - """Raised when another indexing run is already active.""" +from vidxp.core.contracts import INDEX_SCHEMA_VERSION +from vidxp.core.snapshots import IndexSnapshot -def fingerprint_file(path: str | Path) -> dict[str, Any]: - file_path = Path(path) - digest = hashlib.sha256() +INDEX_STATUS_SCHEMA = 1 +INDEX_STATUS_MEDIA_ID_LIMIT = 100 - with file_path.open("rb") as source: - for chunk in iter(lambda: source.read(1024 * 1024), b""): - digest.update(chunk) - stat = file_path.stat() +def bounded_media_ids(media_ids: list[str]) -> dict[str, object]: + selected = media_ids[:INDEX_STATUS_MEDIA_ID_LIMIT] return { - "path": str(file_path.resolve()), - "size": stat.st_size, - "sha256": digest.hexdigest(), + "media_ids": selected, + "media_ids_truncated": len(media_ids) > len(selected), } -def read_index_status(index_directory: str | Path = INDEX_DIRECTORY) -> dict[str, Any] | None: - status_path = Path(index_directory) / INDEX_STATUS_FILE - if not status_path.is_file(): - return None - - try: - return json.loads(status_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - return { - "schema_version": INDEX_STATUS_SCHEMA, - "state": "failed", - "stage": "status", - "message": "The saved index status is unreadable. Re-index the video.", - } - - -def write_index_status( - *, - state: str, - stage: str, - message: str, - video: dict[str, Any] | None = None, - current: int | None = None, - total: int | None = None, - summary: dict[str, Any] | None = None, - error: str | None = None, - index_directory: str | Path = INDEX_DIRECTORY, -) -> dict[str, Any]: - index_path = Path(index_directory) - index_path.mkdir(parents=True, exist_ok=True) - status_path = index_path / INDEX_STATUS_FILE - temporary_path = status_path.with_suffix(".tmp") - - payload: dict[str, Any] = { +def snapshot_status(snapshot: IndexSnapshot) -> dict[str, Any]: + media_ids = sorted(snapshot.generations) + ready = bool(media_ids) + return { "schema_version": INDEX_STATUS_SCHEMA, - "state": state, - "stage": stage, - "message": message, - "updated_at": datetime.now(timezone.utc).isoformat(), + "state": "ready" if ready else "empty", + "stage": "status", + "message": ( + f"The active snapshot contains {len(media_ids)} media item(s)." + if ready + else "The active index snapshot is empty." + ), + "updated_at": snapshot.created_at.isoformat(), + "summary": { + "index_schema_version": INDEX_SCHEMA_VERSION, + "snapshot_id": snapshot.snapshot_id, + "media_count": len(media_ids), + **bounded_media_ids(media_ids), + "modalities": tuple( + snapshot.configuration.get("enabled_modalities", ()) + ), + }, } - if video is not None: - payload["video"] = video - if current is not None: - payload["current"] = current - if total is not None: - payload["total"] = total - if summary is not None: - payload["summary"] = summary - if error is not None: - payload["error"] = error - temporary_path.write_text( - json.dumps(payload, indent=2, sort_keys=True), - encoding="utf-8", - ) - temporary_path.replace(status_path) - return payload + +class IndexNotReadyError(RuntimeError): + """Raised when a search is attempted without a completed index.""" -def require_ready_index( - index_directory: str | Path = INDEX_DIRECTORY, -) -> dict[str, Any]: - status = read_index_status(index_directory) - if status is None: - raise IndexNotReadyError( - "No completed video index was found. Index a video before searching." - ) - if status.get("state") != "ready": - message = status.get("message", "The video index is not ready.") - raise IndexNotReadyError(message) - return status +class IndexingInProgressError(RuntimeError): + """Raised when another indexing run is already active.""" diff --git a/src/vidxp/index_worker.py b/src/vidxp/index_worker.py deleted file mode 100644 index d96a9f4..0000000 --- a/src/vidxp/index_worker.py +++ /dev/null @@ -1,82 +0,0 @@ -from multiprocessing import get_context -from multiprocessing.process import BaseProcess -from threading import Lock - -from vidxp.application import VidXPService -from vidxp.core.contracts import CancellationToken -from vidxp.index_state import IndexingInProgressError -from vidxp.repositories import resolve_repository - -_process: BaseProcess | None = None -_cancel_event = None -_start_lock = Lock() - - -def _configured_service() -> VidXPService: - _, repository = resolve_repository() - return VidXPService( - repository.index_directory, - device=repository.device, - ) - - -def _run_indexing( - path: str, - source_name: str, - cancel_event, - index_directory: str, - device: str | None, - modalities: tuple[str, ...], -) -> None: - VidXPService(index_directory, device=device).create_index( - path, - modalities=modalities, - source_name=source_name, - cancellation=CancellationToken(cancel_event), - ) - - -def indexing_in_progress(service: VidXPService | None = None) -> bool: - active_service = service or _configured_service() - return ( - _process is not None and _process.is_alive() - ) or active_service.indexing_in_progress() - - -def start_indexing( - path: str, - source_name: str, - service: VidXPService | None = None, - *, - modalities: tuple[str, ...], -) -> None: - global _cancel_event, _process - - active_service = service or _configured_service() - with _start_lock: - if indexing_in_progress(active_service): - raise IndexingInProgressError("Another video is already being indexed.") - - context = get_context("spawn") - _cancel_event = context.Event() - _process = context.Process( - target=_run_indexing, - args=( - path, - source_name, - _cancel_event, - str(active_service.index_directory), - active_service.device, - modalities, - ), - name="vidxp-indexer", - daemon=True, - ) - _process.start() - - -def cancel_indexing() -> bool: - if _process is None or not _process.is_alive() or _cancel_event is None: - return False - _cancel_event.set() - return True diff --git a/src/vidxp/infrastructure/__init__.py b/src/vidxp/infrastructure/__init__.py new file mode 100644 index 0000000..80f6700 --- /dev/null +++ b/src/vidxp/infrastructure/__init__.py @@ -0,0 +1 @@ +"""Concrete infrastructure adapters selected by the composition root.""" diff --git a/src/vidxp/infrastructure/dbos_jobs.py b/src/vidxp/infrastructure/dbos_jobs.py new file mode 100644 index 0000000..75559ab --- /dev/null +++ b/src/vidxp/infrastructure/dbos_jobs.py @@ -0,0 +1,455 @@ +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timezone +from time import time_ns +from typing import Any +from uuid import uuid4 + +from dbos import DBOSClient, EnqueueOptions +from pydantic import TypeAdapter, ValidationError + +from vidxp.application_models import ( + Artifact, + ArtifactJobResult, + ErrorDetail, + ErrorCategory, + IndexJobResult, + IndexResult, + Job, + JobKind, + JobPage, + JobProgress, + JobQueue, + JobRequest, + JobState, + ListJobsCommand, + MediaAsset, + MediaImportJobResult, + PrepareModelsJobResult, + PrepareModelsResult, + QueryAnswer, + QueryJobResult, + SearchJobResult, + FusedSearchResult, +) +from vidxp.capabilities.schemas import SearchResult +from vidxp.core.identifiers import JobId +from vidxp.core.cursors import ( + MAX_CURSOR_OFFSET, + CursorError, + decode_cursor, + decode_offset_cursor, + encode_cursor, +) +from vidxp.ports import ( + InvalidJobBackendRequestError, + JobIdempotencyConflictError, +) +from vidxp.search_fusion import fuse_search_results +from vidxp.workflow_contracts import ( + ERROR_EVENT, + PROGRESS_EVENT, + QUEUE_KINDS, + QUEUE_NAMES, + WORKFLOW_CLASS_NAME, + WORKFLOW_INSTANCE_NAME, + WORKFLOW_KINDS, + WORKFLOW_NAMES, + decode_workflow_request, +) + + +_JOB_ID_ADAPTER = TypeAdapter(JobId) +_LIST_SCOPE = "vidxp:jobs" +_WINDOW_END_FIELD = "window_end_ms" +_STATUS_STATES = { + "ENQUEUED": JobState.queued, + "DELAYED": JobState.queued, + "PENDING": JobState.running, + "SUCCESS": JobState.succeeded, + "ERROR": JobState.failed, + "CANCELLED": JobState.cancelled, + "MAX_RECOVERY_ATTEMPTS_EXCEEDED": JobState.recovery_exhausted, +} + + +def _job_id(value: str) -> str: + try: + return _JOB_ID_ADAPTER.validate_python(value) + except ValidationError as exc: + raise InvalidJobBackendRequestError( + "The job identifier is invalid." + ) from exc + + +def _timestamp(value: int | None) -> datetime | None: + if value is None: + return None + return datetime.fromtimestamp(value / 1000, timezone.utc) + + +def _now_ms() -> int: + return time_ns() // 1_000_000 + + +def _decode_search_result(output: Any) -> FusedSearchResult: + try: + return FusedSearchResult.model_validate(output) + except ValidationError: + legacy = SearchResult.model_validate(output) + return fuse_search_results( + query=legacy.query, + requested_modalities=(legacy.modality,), + results=(legacy,), + top_k=max(1, len(legacy.hits)), + ) + + +def _window_end_iso(value: int) -> str: + return datetime.fromtimestamp(value / 1000, timezone.utc).isoformat( + timespec="milliseconds" + ) + + +class DBOSJobBackend: + """Thin projection of DBOS workflow state into VidXP job contracts.""" + + def __init__( + self, + *, + system_database_url: str | None, + application_version: str, + system_database_engine: Any | None = None, + before_access: Callable[[], None] | None = None, + health_check: Callable[[], None] | None = None, + stop_executor: Callable[[], bool] | None = None, + ) -> None: + self.client = DBOSClient( + system_database_url=system_database_url, + system_database_engine=system_database_engine, + dbos_system_schema=( + "dbos" + if system_database_engine is not None + or ( + system_database_url is not None + and not system_database_url.startswith("sqlite:///") + ) + else None + ), + ) + self.application_version = application_version + self.before_access = before_access + self.health_check = health_check + self.stop_executor = stop_executor + + def close(self) -> None: + self.client.destroy() + + def start(self) -> None: + self._prepare_access() + + def submit( + self, + request: JobRequest, + *, + queue: JobQueue, + job_id: str | None = None, + ) -> Job: + self._prepare_access() + identifier = _job_id(job_id or uuid4().hex) + if job_id is not None: + existing = self._status(identifier, load_input=True) + if existing is not None: + return self._idempotent_job(existing, request) + self.client.enqueue( + self._enqueue_options( + request=request, + queue=queue, + identifier=identifier, + ), + request.model_dump(mode="json"), + ) + status = self._status(identifier, load_input=True) + if status is None: + raise RuntimeError("DBOS did not persist the submitted workflow.") + return self._idempotent_job(status, request) + + def enqueue_in_transaction( + self, + connection: Any, + request: JobRequest, + *, + queue: JobQueue, + job_id: str, + ) -> str: + """Enqueue through a caller-owned transaction on the DBOS database.""" + + self._prepare_access() + identifier = _job_id(job_id) + handle = self.client.enqueue_in_transaction( + connection, + self._enqueue_options( + request=request, + queue=queue, + identifier=identifier, + ), + request.model_dump(mode="json"), + ) + if handle.get_workflow_id() != identifier: + raise RuntimeError("DBOS returned an unexpected workflow identity.") + return identifier + + def _enqueue_options( + self, + *, + request: JobRequest, + queue: JobQueue, + identifier: str, + ) -> EnqueueOptions: + return { + "workflow_name": WORKFLOW_NAMES[request.kind], + "queue_name": QUEUE_NAMES[queue], + "workflow_id": identifier, + "app_version": self.application_version, + "attributes": {"vidxp_queue": queue.value}, + "class_name": WORKFLOW_CLASS_NAME, + "instance_name": WORKFLOW_INSTANCE_NAME, + } + + def get(self, job_id: str) -> Job | None: + self._prepare_access() + identifier = _job_id(job_id) + status = self._status(identifier) + if status is None or status.name not in WORKFLOW_KINDS: + return None + return self._job(status) + + def _status( + self, + identifier: str, + *, + load_input: bool = False, + ) -> Any | None: + statuses = self.client.list_workflows( + workflow_ids=[identifier], + limit=1, + load_input=load_input, + load_output=True, + ) + return statuses[0] if statuses else None + + def _idempotent_job(self, status: Any, request: JobRequest) -> Job: + try: + stored = decode_workflow_request(status.input["args"][0]) + except (KeyError, IndexError, TypeError, ValidationError) as exc: + raise JobIdempotencyConflictError from exc + if ( + status.name not in WORKFLOW_KINDS + or WORKFLOW_KINDS[status.name] != request.kind + or stored != request + ): + raise JobIdempotencyConflictError + return self._job(status) + + def list(self, command: ListJobsCommand) -> JobPage: + self._prepare_access() + try: + offset = decode_offset_cursor( + command.cursor, + scope=_LIST_SCOPE, + ) + if command.cursor is None: + window_end_ms = _now_ms() + else: + payload = decode_cursor(command.cursor, _LIST_SCOPE) + window_end_ms = payload.get(_WINDOW_END_FIELD) + if window_end_ms is None: + # Legacy v1 offset cursors did not freeze the result + # window. Preserve them on a best-effort basis, then + # upgrade the next cursor to the stable representation. + window_end_ms = _now_ms() + if ( + not isinstance(window_end_ms, int) + or isinstance(window_end_ms, bool) + or window_end_ms < 0 + or window_end_ms > MAX_CURSOR_OFFSET + ): + raise CursorError("The cursor window is invalid.") + except CursorError as exc: + raise InvalidJobBackendRequestError( + "The job cursor is invalid." + ) from exc + statuses = self.client.list_workflows( + name=list(WORKFLOW_KINDS), + limit=command.page_size + 1, + offset=offset, + sort_desc=True, + end_time=_window_end_iso(window_end_ms), + load_input=False, + load_output=True, + ) + has_more = len(statuses) > command.page_size + selected = statuses[: command.page_size] + next_offset = offset + len(selected) + if next_offset > MAX_CURSOR_OFFSET: + raise InvalidJobBackendRequestError( + "The job cursor is invalid." + ) + return JobPage( + items=tuple(self._job(status) for status in selected), + next_cursor=( + encode_cursor( + _LIST_SCOPE, + { + "offset": next_offset, + _WINDOW_END_FIELD: window_end_ms, + }, + ) + if has_more + else None + ), + ) + + def cancel(self, job_id: str) -> Job | None: + self._prepare_access() + current = self.get(job_id) + if current is None: + return None + self.client.cancel_workflow(current.job_id) + return self.get(current.job_id) + + def retry( + self, + job_id: str, + *, + retry_id: str | None = None, + ) -> Job | None: + self._prepare_access() + current = self.get(job_id) + if current is None: + return None + status = self._status(current.job_id, load_input=True) + if status is None: + return None + if retry_id is not None or status.name == "vidxp.search.v1": + try: + request = decode_workflow_request(status.input["args"][0]) + except (KeyError, IndexError, TypeError, ValidationError) as exc: + raise InvalidJobBackendRequestError from exc + return self.submit( + request, + queue=current.queue, + job_id=retry_id, + ) + handle = self.client.fork_workflow( + current.job_id, + 1, + application_version=self.application_version, + queue_name=QUEUE_NAMES[current.queue], + ) + retried_id = handle.get_workflow_id() + return self.get(retried_id) + + def health(self) -> None: + if self.health_check is not None: + self.health_check() + self.client.list_workflows(limit=1, load_input=False, load_output=False) + + def stop_worker(self) -> bool: + if self.stop_executor is None: + raise RuntimeError( + "This workflow backend does not own a local worker." + ) + return self.stop_executor() + + def _prepare_access(self) -> None: + if self.before_access is not None: + self.before_access() + + def _job(self, status: Any) -> Job: + kind = WORKFLOW_KINDS[status.name] + recorded_queue = (status.attributes or {}).get("vidxp_queue") + try: + queue = ( + JobQueue(recorded_queue) + if recorded_queue is not None + else QUEUE_KINDS.get(status.queue_name) + ) + except ValueError: + queue = None + if queue is None: + raise RuntimeError("A VidXP workflow has an unexpected queue.") + state = _STATUS_STATES[status.status] + progress = self._event(status.workflow_id, PROGRESS_EVENT, JobProgress) + error = ( + self._event(status.workflow_id, ERROR_EVENT, ErrorDetail) + if state + in { + JobState.failed, + JobState.cancelled, + JobState.recovery_exhausted, + } + else None + ) + result = None + if state == JobState.succeeded: + if kind == JobKind.media_import: + result = MediaImportJobResult( + result=MediaAsset.model_validate(status.output) + ) + elif kind == JobKind.index: + result = IndexJobResult( + result=IndexResult.model_validate(status.output) + ) + elif kind == JobKind.search: + result = SearchJobResult( + result=_decode_search_result(status.output) + ) + elif kind == JobKind.query: + result = QueryJobResult( + result=QueryAnswer.model_validate(status.output) + ) + elif kind in {JobKind.snippet, JobKind.actor_overlay}: + result = ArtifactJobResult( + kind=kind, + result=Artifact.model_validate(status.output), + ) + else: + result = PrepareModelsJobResult( + result=PrepareModelsResult.model_validate(status.output) + ) + if error is None and state == JobState.failed: + error = ErrorDetail( + code="job_execution_failed", + category=ErrorCategory.internal, + message="The background job failed unexpectedly.", + ) + elif error is None and state == JobState.recovery_exhausted: + error = ErrorDetail( + code="job_recovery_exhausted", + category=ErrorCategory.unavailable, + message="The background job exhausted its recovery attempts.", + retryable=True, + ) + return Job( + job_id=status.workflow_id, + kind=kind, + state=state, + queue=queue, + progress=progress, + result=result, + error=error, + recovery_attempts=status.recovery_attempts or 0, + created_at=_timestamp(status.created_at), + updated_at=_timestamp(status.updated_at), + ) + + def _event(self, job_id: str, key: str, model: type[Any]) -> Any | None: + payload = self.client.get_event(job_id, key, timeout_seconds=0) + if payload is None: + return None + try: + return model.model_validate(payload) + except ValidationError: + return None diff --git a/src/vidxp/infrastructure/dbos_workflows.py b/src/vidxp/infrastructure/dbos_workflows.py new file mode 100644 index 0000000..898c4a9 --- /dev/null +++ b/src/vidxp/infrastructure/dbos_workflows.py @@ -0,0 +1,359 @@ +from __future__ import annotations + +import logging +from collections.abc import Callable +from datetime import datetime, timezone +from time import monotonic +from typing import Any + +from dbos import DBOS, DBOSConfiguredInstance +from pydantic import TypeAdapter + +from vidxp.application import VidXPApplication +from vidxp.application_models import ( + ActorOverlayJobRequest, + ApplicationError, + ErrorCategory, + ErrorDetail, + IndexJobRequest, + JobKind, + JobProgress, + MediaImportJobRequest, + SnippetJobRequest, + PrepareModelsJobRequest, + QueryJobRequest, + SearchJobRequest, +) +from vidxp.core.contracts import CancellationToken +from vidxp.core.contracts import IndexCancelledError +from vidxp.execution import ExecutionContext +from vidxp.workflow_contracts import ( + ERROR_EVENT, + PROGRESS_EVENT, + WORKFLOW_CLASS_NAME, + WORKFLOW_INSTANCE_NAME, + WORKFLOW_NAMES, + decode_workflow_request, +) + + +_ARTIFACT_REQUEST = TypeAdapter( + ActorOverlayJobRequest | SnippetJobRequest +) +LOGGER = logging.getLogger(__name__) + + +class _DBOSCancellationEvent: + def __init__(self, poll_interval_seconds: float = 0.2) -> None: + self._poll_interval_seconds = poll_interval_seconds + self._checked_at = 0.0 + self._cancelled = False + + def is_set(self) -> bool: + if self._cancelled: + return True + now = monotonic() + if now - self._checked_at < self._poll_interval_seconds: + return False + self._checked_at = now + status = DBOS.get_workflow_status(DBOS.workflow_id) + self._cancelled = status is not None and status.status == "CANCELLED" + return self._cancelled + + def set(self) -> None: + if DBOS.workflow_id is not None: + DBOS.cancel_workflow(DBOS.workflow_id) + self._cancelled = True + + +def _publish_progress(event: dict[str, Any]) -> None: + stage = str(event.get("stage") or "working")[:128] + message = str(event.get("message") or stage.replace("_", " "))[:512] + current = event.get("current") + total = event.get("total") + progress = JobProgress( + stage=stage, + message=message, + current=current if isinstance(current, int) else None, + total=total if isinstance(total, int) and total > 0 else None, + updated_at=datetime.now(timezone.utc), + ) + DBOS.set_event(PROGRESS_EVENT, progress.model_dump(mode="json")) + + +def _execution() -> ExecutionContext: + cancellation = CancellationToken(_DBOSCancellationEvent()) + + def publish(event: dict[str, Any]) -> None: + cancellation.raise_if_cancelled() + _publish_progress(event) + + return ExecutionContext( + job_id=DBOS.workflow_id, + progress=publish, + cancellation=cancellation, + ) + + +def _publish_error(exc: Exception) -> None: + if isinstance(exc, ApplicationError): + error = exc.detail + else: + error = ErrorDetail( + code="job_execution_failed", + category=ErrorCategory.internal, + message="The background job failed unexpectedly.", + ) + DBOS.set_event(ERROR_EVENT, error.model_dump(mode="json")) + + +def _step_boundary( + operation: Callable[[], dict[str, Any]], +) -> dict[str, Any]: + try: + return operation() + except IndexCancelledError: + raise + except Exception as exc: + if isinstance(exc, ApplicationError): + LOGGER.warning( + "VidXP workflow %s failed with %s.", + DBOS.workflow_id, + exc.detail.code, + ) + else: + LOGGER.exception( + "VidXP workflow %s failed unexpectedly.", + DBOS.workflow_id, + ) + _publish_error(exc) + raise RuntimeError("VidXP job execution failed.") from exc + + +@DBOS.dbos_class(class_name=WORKFLOW_CLASS_NAME) +class VidXPWorkerWorkflows(DBOSConfiguredInstance): + """Official DBOS configured instance for one composed worker application.""" + + def __init__(self, application: VidXPApplication) -> None: + self.application = application + super().__init__(WORKFLOW_INSTANCE_NAME) + + @DBOS.step(name="vidxp.run_index.v1") + def run_index_step(self, payload: dict[str, Any]) -> dict[str, Any]: + def execute() -> dict[str, Any]: + request = IndexJobRequest.model_validate(payload) + execution = _execution() + _publish_progress( + { + "stage": "starting", + "message": "Starting the indexing job.", + } + ) + result = self.application.create_index( + request.command, + execution=execution, + ) + _publish_progress( + { + "stage": "complete", + "message": "The index generation is ready.", + "current": 1, + "total": 1, + } + ) + return result.model_dump(mode="json") + + return _step_boundary(execute) + + @DBOS.step(name="vidxp.run_media_import.v1") + def run_media_import_step( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + def execute() -> dict[str, Any]: + request = MediaImportJobRequest.model_validate(payload) + _publish_progress( + { + "stage": "importing", + "message": "Validating and importing the completed upload.", + } + ) + result = self.application.import_completed_upload( + request.upload_id + ) + _publish_progress( + { + "stage": "complete", + "message": "The uploaded media is ready.", + "current": 1, + "total": 1, + } + ) + return result.model_dump(mode="json") + + return _step_boundary(execute) + + @DBOS.step(name="vidxp.run_artifact.v1") + def run_artifact_step(self, payload: dict[str, Any]) -> dict[str, Any]: + def execute() -> dict[str, Any]: + request = _ARTIFACT_REQUEST.validate_python(payload) + execution = _execution() + if isinstance(request, SnippetJobRequest): + result = self.application.create_snippet( + request.command, + execution=execution, + ) + else: + result = self.application.render_actor( + request.command, + snapshot=request.snapshot, + execution=execution, + ) + return result.model_dump(mode="json") + + return _step_boundary(execute) + + def _run_search(self, payload: dict[str, Any]) -> dict[str, Any]: + def execute() -> dict[str, Any]: + request = decode_workflow_request(payload) + if not isinstance(request, SearchJobRequest): + raise ValueError("A search workflow requires a search request.") + execution = _execution() + _publish_progress( + { + "stage": "searching", + "message": "Searching the committed index.", + } + ) + execution.checkpoint() + result = self.application.search( + request.command, + snapshot=request.snapshot, + ) + execution.checkpoint() + _publish_progress( + { + "stage": "complete", + "message": "Search results are ready.", + "current": 1, + "total": 1, + } + ) + return result.model_dump(mode="json") + + return _step_boundary(execute) + + @DBOS.step(name="vidxp.run_search.v1") + def run_legacy_search_step( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return self._run_search(payload) + + @DBOS.step(name="vidxp.run_search.v2") + def run_search_step(self, payload: dict[str, Any]) -> dict[str, Any]: + return self._run_search(payload) + + @DBOS.step(name="vidxp.run_query.v1") + def run_query_step(self, payload: dict[str, Any]) -> dict[str, Any]: + def execute() -> dict[str, Any]: + request = QueryJobRequest.model_validate(payload) + execution = _execution() + _publish_progress( + { + "stage": "querying", + "message": "Querying the committed index.", + } + ) + execution.checkpoint() + result = self.application.query_video( + request.command, + snapshot=request.snapshot, + execution=execution, + ) + execution.checkpoint() + _publish_progress( + { + "stage": "complete", + "message": "The grounded query result is ready.", + "current": 1, + "total": 1, + } + ) + return result.model_dump(mode="json") + + return _step_boundary(execute) + + @DBOS.step(name="vidxp.prepare_models_step.v1") + def prepare_models_step( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + def execute() -> dict[str, Any]: + request = PrepareModelsJobRequest.model_validate(payload) + execution = _execution() + _publish_progress( + { + "stage": "preparing_models", + "message": "Preparing model artifacts.", + } + ) + result = self.application.prepare_models( + request.command, + execution=execution, + ) + _publish_progress( + { + "stage": "complete", + "message": "Model artifacts are ready.", + "current": 1, + "total": 1, + } + ) + return result.model_dump(mode="json") + + return _step_boundary(execute) + + @DBOS.workflow(name=WORKFLOW_NAMES[JobKind.index]) + def index_workflow(self, payload: dict[str, Any]) -> dict[str, Any]: + return self.run_index_step(payload) + + @DBOS.workflow(name=WORKFLOW_NAMES[JobKind.media_import]) + def media_import_workflow( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return self.run_media_import_step(payload) + + @DBOS.workflow(name=WORKFLOW_NAMES[JobKind.snippet]) + def snippet_workflow(self, payload: dict[str, Any]) -> dict[str, Any]: + return self.run_artifact_step(payload) + + @DBOS.workflow(name=WORKFLOW_NAMES[JobKind.search]) + def search_workflow(self, payload: dict[str, Any]) -> dict[str, Any]: + return self.run_search_step(payload) + + @DBOS.workflow(name="vidxp.search.v1") + def legacy_search_workflow( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return self.run_legacy_search_step(payload) + + @DBOS.workflow(name=WORKFLOW_NAMES[JobKind.query]) + def query_workflow(self, payload: dict[str, Any]) -> dict[str, Any]: + return self.run_query_step(payload) + + @DBOS.workflow(name=WORKFLOW_NAMES[JobKind.actor_overlay]) + def actor_overlay_workflow( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return self.run_artifact_step(payload) + + @DBOS.workflow(name=WORKFLOW_NAMES[JobKind.prepare_models]) + def prepare_models_workflow( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return self.prepare_models_step(payload) diff --git a/src/vidxp/infrastructure/local_artifacts.py b/src/vidxp/infrastructure/local_artifacts.py new file mode 100644 index 0000000..872d893 --- /dev/null +++ b/src/vidxp/infrastructure/local_artifacts.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path +from time import monotonic, sleep + +from pydantic import TypeAdapter + +from vidxp.core.artifacts import ( + ArtifactRenderError, + ArtifactRendererUnavailableError, + StagedArtifact, + StoredArtifact, +) +from vidxp.core.contracts import CancellationToken +from vidxp.core.identifiers import ArtifactId +from vidxp.core.indexing_common import ProgressCallback +from vidxp.core.video import render_actor_video +from vidxp.core.manifest import sha256_file +from vidxp.infrastructure.local_files import ( + prepare_managed_destination, +) +from vidxp.infrastructure.local_objects import LocalObjectStore + + +class LocalArtifactStore: + def __init__(self, root: Path) -> None: + self.root = root + self.objects = root / "objects" + self.staging = root / ".staging" + self._managed = LocalObjectStore(root) + + def stage(self, artifact_id: str, *, suffix: str) -> StagedArtifact: + validated_id = TypeAdapter(ArtifactId).validate_python(artifact_id) + if ( + not suffix.startswith(".") + or not suffix[1:].isalnum() + or len(suffix) > 10 + ): + raise ValueError("Artifact suffix is invalid.") + path = prepare_managed_destination( + self.root, + f".staging/{validated_id}.tmp{suffix.lower()}", + ) + path.unlink(missing_ok=True) + return StagedArtifact(artifact_id=artifact_id, path=path) + + def publish(self, staged: StagedArtifact) -> StoredArtifact: + suffix = staged.path.suffix.lower() + storage_key = ( + f"objects/{staged.artifact_id[:2]}/" + f"{staged.artifact_id}{suffix}" + ) + path, checksum, byte_size = self._managed.publish( + staged.path, + storage_key, + expected_sha256=None, + replace_corrupt=False, + ) + return StoredArtifact( + sha256=checksum, + byte_size=byte_size, + storage_key=storage_key, + local_path=path, + ) + + def recover( + self, + artifact_id: str, + *, + suffix: str, + ) -> StoredArtifact | None: + validated_id = TypeAdapter(ArtifactId).validate_python(artifact_id) + storage_key = ( + f"objects/{validated_id[:2]}/" + f"{validated_id}{suffix.lower()}" + ) + try: + path = self._managed.resolve(storage_key) + except FileNotFoundError: + return None + return StoredArtifact( + sha256=sha256_file(path), + byte_size=path.stat().st_size, + storage_key=storage_key, + local_path=path, + ) + + def discard(self, staged: StagedArtifact) -> None: + staged.path.unlink(missing_ok=True) + + def delete(self, storage_key: str) -> None: + self._managed.delete(storage_key) + + def verify( + self, + storage_key: str, + *, + sha256: str, + byte_size: int, + ) -> Path: + return self._managed.verify( + storage_key, + sha256=sha256, + byte_size=byte_size, + ) + + def resolve(self, storage_key: str) -> Path: + self.objects.mkdir(parents=True, exist_ok=True) + return self._managed.resolve(storage_key) + + +class LocalActorRenderer: + def render( + self, + source: Path, + destination: Path, + cluster_id: str, + detections: list[dict], + *, + cancellation: CancellationToken, + progress: ProgressCallback | None, + ) -> None: + render_actor_video( + source, + destination, + cluster_id, + detections, + cancellation=cancellation, + progress=progress, + ) + + +class FFmpegSnippetRenderer: + def __init__( + self, + executable: str = "ffmpeg", + *, + timeout_seconds: float = 600, + ) -> None: + self.executable = executable + self.timeout_seconds = timeout_seconds + + def render( + self, + source: Path, + destination: Path, + *, + start_seconds: float, + end_seconds: float, + compatible_mp4: bool, + cancellation: CancellationToken, + progress: ProgressCallback | None, + ) -> None: + duration = end_seconds - start_seconds + encoding = ( + ["-c:v", "libx264", "-c:a", "aac", "-movflags", "+faststart"] + if compatible_mp4 + else ["-c", "copy"] + ) + try: + process = subprocess.Popen( + [ + self.executable, + "-nostdin", + "-v", + "error", + "-protocol_whitelist", + "file,pipe", + "-ss", + str(start_seconds), + "-i", + str(source), + "-t", + str(duration), + *encoding, + str(destination), + ], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError as exc: + raise ArtifactRendererUnavailableError( + "The configured ffmpeg executable is unavailable." + ) from exc + + started_at = monotonic() + last_progress_at = started_at + try: + while process.poll() is None: + cancellation.raise_if_cancelled() + now = monotonic() + if now - started_at > self.timeout_seconds: + raise ArtifactRenderError( + "The requested snippet render timed out." + ) + if progress is not None and now - last_progress_at >= 1: + progress( + { + "state": "rendering", + "stage": "rendering", + "message": "Rendering the requested video snippet.", + } + ) + last_progress_at = now + sleep(0.1) + + if process.returncode != 0: + raise ArtifactRenderError( + "The requested snippet could not be rendered." + ) + except BaseException: + if process.poll() is None: + self._stop(process) + raise + + @staticmethod + def _stop(process: subprocess.Popen) -> None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) diff --git a/src/vidxp/infrastructure/local_catalog.py b/src/vidxp/infrastructure/local_catalog.py new file mode 100644 index 0000000..5d337b8 --- /dev/null +++ b/src/vidxp/infrastructure/local_catalog.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from pathlib import Path + +from sqlalchemy import Column, Integer, MetaData, Table, insert, select, update + +from vidxp.infrastructure.sql_catalog import SQLCatalog +from vidxp.infrastructure.sql_tables import ( + artifact_requests, + artifacts, + media, + media_import_requests, + metadata, +) + +CATALOG_SCHEMA_VERSION = 3 +_local_metadata = MetaData() +catalog_metadata = Table( + "catalog_metadata", + _local_metadata, + Column("schema_version", Integer, nullable=False), +) + + +class LocalCatalog(SQLCatalog): + """Repository-scoped SQLite catalog using the shared SQL adapter.""" + + def __init__(self, database: Path) -> None: + database.parent.mkdir(parents=True, exist_ok=True) + super().__init__( + f"sqlite:///{database.resolve().as_posix()}", + initialize=False, + ) + metadata.create_all( + self.engine, + tables=( + media, + artifacts, + artifact_requests, + media_import_requests, + ), + ) + _local_metadata.create_all(self.engine) + with self.transaction() as connection: + version = connection.execute( + select(catalog_metadata.c.schema_version) + ).scalar_one_or_none() + if version is None: + connection.execute( + insert(catalog_metadata).values( + schema_version=CATALOG_SCHEMA_VERSION + ) + ) + elif version in {1, 2}: + connection.execute( + update(catalog_metadata).values( + schema_version=CATALOG_SCHEMA_VERSION + ) + ) + elif version != CATALOG_SCHEMA_VERSION: + raise RuntimeError( + "The repository catalog schema is incompatible." + ) diff --git a/src/vidxp/infrastructure/local_files.py b/src/vidxp/infrastructure/local_files.py new file mode 100644 index 0000000..66650c9 --- /dev/null +++ b/src/vidxp/infrastructure/local_files.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from pathlib import Path, PurePosixPath + +from vidxp.core.manifest import sync_parent_directory +from vidxp.core.storage_keys import validate_storage_key + + +def resolve_managed_file(root: Path, storage_key: str) -> Path: + """Resolve an internal storage key without permitting link escapes.""" + + validate_storage_key(storage_key) + resolved_root = root.resolve(strict=True) + relative = PurePosixPath(storage_key) + candidate = root.joinpath(*relative.parts) + resolved = candidate.resolve(strict=True) + if not resolved.is_relative_to(resolved_root) or not resolved.is_file(): + raise FileNotFoundError("The managed file is unavailable.") + + current = root + for part in relative.parts: + current = current / part + is_junction = getattr(current, "is_junction", lambda: False) + if current.is_symlink() or is_junction(): + raise PermissionError("Managed storage links are not permitted.") + return resolved + + +def prepare_managed_destination(root: Path, storage_key: str) -> Path: + """Create a confined parent tree and reject pre-existing links.""" + + validate_storage_key(storage_key) + _ensure_directory(root) + resolved_root = root.resolve(strict=True) + relative = PurePosixPath(storage_key) + current = root + for part in relative.parts[:-1]: + current = current / part + if current.exists(): + is_junction = getattr(current, "is_junction", lambda: False) + if current.is_symlink() or is_junction() or not current.is_dir(): + raise PermissionError( + "Managed storage parent links are not permitted." + ) + else: + _ensure_directory(current) + if not current.resolve(strict=True).is_relative_to(resolved_root): + raise PermissionError("Managed storage escaped its configured root.") + + destination = current / relative.name + if destination.exists(): + is_junction = getattr(destination, "is_junction", lambda: False) + if destination.is_symlink() or is_junction() or not destination.is_file(): + raise PermissionError("Managed storage links are not permitted.") + return destination + + +def _ensure_directory(path: Path) -> None: + missing = [] + current = path + while not current.exists(): + missing.append(current) + current = current.parent + for directory in reversed(missing): + try: + directory.mkdir() + except FileExistsError: + if not directory.is_dir(): + raise + else: + sync_parent_directory(directory.parent) + + +def durable_replace(source: Path, destination: Path) -> None: + """Replace a file and synchronize both affected directory entries.""" + + source_parent = source.parent + destination_parent = destination.parent + source.replace(destination) + sync_parent_directory(destination_parent) + if source_parent != destination_parent: + sync_parent_directory(source_parent) + + +def durable_unlink(path: Path, *, missing_ok: bool = False) -> bool: + """Remove a file and synchronize the removed directory entry.""" + + try: + path.unlink() + except FileNotFoundError: + if missing_ok: + return False + raise + sync_parent_directory(path.parent) + return True diff --git a/src/vidxp/infrastructure/local_index.py b/src/vidxp/infrastructure/local_index.py new file mode 100644 index 0000000..e333d3e --- /dev/null +++ b/src/vidxp/infrastructure/local_index.py @@ -0,0 +1,575 @@ +from __future__ import annotations + +import json +import shutil +from dataclasses import replace +from pathlib import Path +from typing import Any +from uuid import UUID + +from vidxp.capabilities.contracts import ( + RuntimeCheckBinding, + module_import_check, +) +from vidxp.capabilities.registry import CapabilityRegistry +from vidxp.core.contracts import ( + CancellationToken, + IndexConfig, + IndexSchemaError, +) +from vidxp.core.indexing_common import ProgressCallback +from vidxp.core.manifest import MANIFEST_FILE, ManifestStore +from vidxp.core.runner import index_video +from vidxp.core.storage import ( + ChromaClientFactory, + IndexStorage, + SnapshotScopedIndexStore, +) +from vidxp.infrastructure.local_snapshots import LocalSnapshotRepository +from vidxp.repository_layout import RepositoryLayout +from vidxp.runtime import ModelRuntime + + +def _index_runtime_checks( + chroma_client: str, +) -> tuple[RuntimeCheckBinding, ...]: + return ( + RuntimeCheckBinding( + capability="storage", + check=module_import_check( + "Chroma storage import", + "chromadb", + chroma_client, + ), + ), + RuntimeCheckBinding( + capability="storage", + check=module_import_check( + "Host resource monitor import", + "psutil", + "virtual_memory", + ), + ), + ) + + +LOCAL_INDEX_RUNTIME_CHECKS = _index_runtime_checks("PersistentClient") +SERVER_INDEX_RUNTIME_CHECKS = _index_runtime_checks("HttpClient") + + +class LocalIndexReader: + """Model-free access to immutable local index snapshots.""" + + def __init__( + self, + layout: RepositoryLayout, + *, + chroma_server_url: str | None = None, + snapshot_repository: LocalSnapshotRepository | None = None, + ) -> None: + self.layout = layout + self.repository = snapshot_repository or LocalSnapshotRepository( + layout.indexes + ) + self.chroma_clients = ChromaClientFactory(chroma_server_url) + + def _require_index_directory(self, index_directory: str | Path) -> None: + if Path(index_directory) != self.layout.indexes: + raise ValueError( + "The index operation is outside the configured repository." + ) + + def active_config( + self, + index_directory: Path, + *, + device: str, + ) -> IndexConfig: + self._require_index_directory(index_directory) + config, _snapshot = self.repository.active_config(device=device) + return config + + def config_for_snapshot( + self, + index_directory: Path, + *, + snapshot_id: str, + snapshot_sha256: str, + device: str, + ) -> IndexConfig: + self._require_index_directory(index_directory) + return self.repository.config_for_snapshot( + snapshot_id, + snapshot_sha256=snapshot_sha256, + device=device, + ) + + def open_store(self, config: IndexConfig) -> SnapshotScopedIndexStore: + if config.snapshot_id is None: + raise IndexSchemaError("A snapshot ID is required for index reads.") + if config.snapshot_sha256 is None: + raise IndexSchemaError( + "A snapshot checksum is required for index reads." + ) + snapshot = self.repository.read_snapshot( + config.snapshot_id, + expected_sha256=config.snapshot_sha256, + ) + generation_ids = tuple( + reference.generation_id + for reference in snapshot.generations.values() + ) + storage = self._open_committed_storage(config) + return SnapshotScopedIndexStore( + storage, + generation_ids=generation_ids, + ) + + @classmethod + def _validate_snapshot_storage( + cls, + storage: IndexStorage, + snapshot, + base_config: IndexConfig | None = None, + ) -> None: + active_config = base_config or storage.config + for reference in snapshot.generations.values(): + try: + actual = { + modality: storage.count_records( + modality, + video_id=reference.media_id, + generation_ids=(reference.generation_id,), + ) + for modality in active_config.enabled_modalities + } + except FileNotFoundError as exc: + raise IndexSchemaError( + "A committed Chroma collection is missing." + ) from exc + if actual != dict(reference.record_counts): + raise IndexSchemaError( + "Stored generation record counts do not match the " + "authoritative manifest." + ) + + def _open_committed_storage(self, config: IndexConfig) -> IndexStorage: + try: + return IndexStorage( + config, + create=False, + client_factory=self.chroma_clients, + ) + except FileNotFoundError as exc: + raise IndexSchemaError( + "The committed Chroma store is missing." + ) from exc + + +class LocalIndexBackend(LocalIndexReader): + def __init__( + self, + registry: CapabilityRegistry, + runtime: ModelRuntime, + layout: RepositoryLayout, + *, + chroma_server_url: str | None = None, + snapshot_repository: LocalSnapshotRepository | None = None, + ) -> None: + super().__init__( + layout, + chroma_server_url=chroma_server_url, + snapshot_repository=snapshot_repository, + ) + self.registry = registry + self.runtime = runtime + + def status(self, index_directory: Path) -> dict[str, Any] | None: + self._require_index_directory(index_directory) + status = self.repository.status() + if status is not None and status.get("state") == "ready": + config, snapshot = self.repository.active_config( + device=self.runtime.backends.torch_device + ) + with self._open_committed_storage(config) as storage: + self._validate_snapshot_storage(storage, snapshot) + return status + + def create( + self, + path: Path, + *, + config: IndexConfig, + progress: ProgressCallback | None, + cancellation: CancellationToken | None, + source_name: str | None, + source_checksum: str, + operation_id: str | None = None, + ) -> dict[str, Any]: + self._require_index_directory(config.index_directory) + repository = self.repository + media_id = config.video_id + if media_id is None: + raise ValueError("A catalog media_id is required for indexing.") + generation_id = operation_id or repository.new_generation_id() + generation_directory = repository.generation_directory(generation_id) + build_config = replace( + config, + video_id=media_id, + storage_directory=repository.store, + generation_directory=generation_directory, + generation_id=generation_id, + snapshot_id=None, + snapshot_sha256=None, + ) + + with repository.lease(): + active = repository.require_compatible_profile( + media_id=media_id, + config_fingerprint=build_config.fingerprint(), + ) + if active is not None and active.generations: + active_config, _ = repository.active_config( + device=build_config.device + ) + with self._open_committed_storage( + active_config + ) as active_store: + self._validate_snapshot_storage( + active_store, + active, + active_config, + ) + if operation_id is not None and active is not None: + committed = active.generations.get(media_id) + if ( + committed is not None + and committed.generation_id == operation_id + ): + if ( + committed.input_sha256 != source_checksum + or committed.config_fingerprint + != build_config.fingerprint() + ): + raise IndexSchemaError( + "The indexing operation ID is already bound to " + "different input or configuration." + ) + return { + "media_id": media_id, + "generation_id": committed.generation_id, + "snapshot_id": active.snapshot_id, + "active_media_count": len(active.generations), + "record_counts": dict(committed.record_counts), + } + completed_manifest = generation_directory / MANIFEST_FILE + if operation_id is not None and completed_manifest.is_file(): + completed = None + try: + completed = repository.generation_reference( + generation_id=operation_id, + media_id=media_id, + ) + except IndexSchemaError: + try: + raw_manifest = json.loads( + completed_manifest.read_text(encoding="utf-8") + ) + except (OSError, json.JSONDecodeError): + pass + else: + if raw_manifest.get("state") == "complete": + manifest_store = ManifestStore( + build_config, + registry=self.registry, + runtime=self.runtime, + ) + with IndexStorage( + build_config, + create=False, + client_factory=self.chroma_clients, + ) as storage: + recovered_counts = ( + self._validate_generation_records( + storage, + build_config, + ) + ) + manifest_store.record_storage_counts( + recovered_counts + ) + completed = repository.generation_reference( + generation_id=operation_id, + media_id=media_id, + ) + if completed is not None: + if ( + completed.input_sha256 != source_checksum + or completed.config_fingerprint + != build_config.fingerprint() + ): + raise IndexSchemaError( + "The indexing operation ID is already bound to " + "different input or configuration." + ) + snapshot = repository.publish_generation( + completed, + build_config, + ) + return { + "media_id": media_id, + "generation_id": completed.generation_id, + "snapshot_id": snapshot.snapshot_id, + "active_media_count": len(snapshot.generations), + "record_counts": dict(completed.record_counts), + } + self._cleanup_abandoned_generations( + repository, + build_config, + client_factory=self.chroma_clients, + ) + generation_validated = False + try: + manifest_store = ManifestStore( + build_config, + registry=self.registry, + runtime=self.runtime, + ) + with IndexStorage( + build_config, + client_factory=self.chroma_clients, + ) as storage: + index_video( + str(path), + progress_callback=progress, + source_name=source_name, + checksum=source_checksum, + config=build_config, + cancellation=cancellation, + storage=storage, + manifest_store=manifest_store, + registry=self.registry, + runtime=self.runtime, + ) + if cancellation is not None: + cancellation.raise_if_cancelled() + record_counts = self._validate_generation_records( + storage, + build_config, + ) + manifest_store.record_storage_counts(record_counts) + if cancellation is not None: + cancellation.raise_if_cancelled() + reference = repository.generation_reference( + generation_id=generation_id, + media_id=media_id, + ) + generation_validated = True + if cancellation is not None: + cancellation.raise_if_cancelled() + snapshot = repository.publish_generation( + reference, + build_config, + ) + except BaseException: + try: + self._remove_uncommitted_generation( + repository, + build_config, + preserve_completed=generation_validated, + ) + except Exception: + # Preserve the primary build/publication failure. + pass + raise + + return { + "media_id": media_id, + "generation_id": generation_id, + "snapshot_id": snapshot.snapshot_id, + "active_media_count": len(snapshot.generations), + "record_counts": record_counts, + } + + @staticmethod + def _validate_generation_records( + storage: IndexStorage, + config: IndexConfig, + expected_counts: dict[str, int] | None = None, + ) -> dict[str, int]: + if config.generation_id is None or config.video_id is None: + raise IndexSchemaError( + "Generation and media identity are required before publication." + ) + counts: dict[str, int] = {} + for modality in config.enabled_modalities: + records = storage.records( + modality, + generation_ids=(config.generation_id,), + ) + if any( + record.get("generation_id") != config.generation_id + or record.get("video_id") != config.video_id + or record.get("modality") != modality + for record in records + ): + raise IndexSchemaError( + f"Stored {modality} records do not match the generation " + "being published." + ) + counts[modality] = len(records) + if expected_counts is not None and counts != expected_counts: + raise IndexSchemaError( + "Stored generation record counts do not match the " + "authoritative manifest." + ) + return counts + + @staticmethod + def _cleanup_abandoned_generations( + repository: LocalSnapshotRepository, + config: IndexConfig, + *, + client_factory: ChromaClientFactory | None = None, + ) -> None: + clients = client_factory or ChromaClientFactory() + completed: set[str] = set() + incomplete: dict[str, Path] = {} + if repository.generations.is_dir(): + for path in repository.generations.iterdir(): + if ( + not path.is_dir() + or not LocalIndexBackend._is_generation_id(path.name) + ): + continue + manifest_path = path / MANIFEST_FILE + try: + manifest = json.loads( + manifest_path.read_text(encoding="utf-8") + ) + except (OSError, json.JSONDecodeError): + incomplete[path.name] = path + continue + if ( + manifest.get("state") == "complete" + and manifest.get("generation_id") == path.name + ): + completed.add(path.name) + else: + incomplete[path.name] = path + + cleanup_config = replace( + config, + storage_directory=repository.store, + generation_directory=None, + video_id=None, + generation_id=None, + snapshot_id=None, + snapshot_sha256=None, + ) + if clients.remote or repository.store.is_dir(): + try: + with IndexStorage( + cleanup_config, + create=False, + client_factory=clients, + ) as storage: + stored_ids: set[str] = set() + for modality in cleanup_config.enabled_modalities: + try: + records = storage.records(modality) + except FileNotFoundError: + continue + for record in records: + generation_id = record.get("generation_id") + if ( + isinstance(generation_id, str) + and LocalIndexBackend._is_generation_id( + generation_id + ) + ): + stored_ids.add(generation_id) + for generation_id in sorted( + set(incomplete) | (stored_ids - completed) + ): + for modality in cleanup_config.enabled_modalities: + try: + storage.delete_generation( + generation_id, + modalities=(modality,), + ) + except FileNotFoundError: + continue + except FileNotFoundError: + pass + + for generation_id, path in incomplete.items(): + resolved = path.resolve() + if repository.generations.resolve() not in resolved.parents: + raise RuntimeError( + "Refusing to clean a generation outside the repository." + ) + shutil.rmtree(resolved) + + @staticmethod + def _is_generation_id(value: str) -> bool: + try: + identifier = UUID(hex=value) + except ValueError: + return False + return identifier.version == 4 and identifier.hex == value + + def _remove_uncommitted_generation( + self, + repository: LocalSnapshotRepository, + config: IndexConfig, + *, + preserve_completed: bool, + ) -> None: + generation_id = config.generation_id + if generation_id is None: + return + generation_directory = config.run_directory.resolve() + generations_root = repository.generations.resolve() + if generations_root not in generation_directory.parents: + raise RuntimeError( + "Refusing to clean a generation outside the repository." + ) + manifest_path = generation_directory / MANIFEST_FILE + state = None + if manifest_path.is_file(): + try: + state = json.loads( + manifest_path.read_text(encoding="utf-8") + ).get("state") + except (OSError, json.JSONDecodeError): + state = None + if state == "complete" and preserve_completed: + return + try: + with IndexStorage( + config, + client_factory=self.chroma_clients, + ) as storage: + storage.delete_generation(generation_id) + except Exception: + # Cleanup is best-effort and must never replace the build failure. + pass + if generation_directory.is_dir(): + shutil.rmtree(generation_directory) + + def indexing_in_progress(self, config: IndexConfig) -> bool: + self._require_index_directory(config.index_directory) + return self.repository.mutation_in_progress() + + def remove(self, config: IndexConfig, media_id: str) -> bool: + self._require_index_directory(config.index_directory) + repository = self.repository + with repository.lease(): + return repository.remove(media_id) + + def clear(self, config: IndexConfig) -> bool: + self._require_index_directory(config.index_directory) + repository = self.repository + with repository.lease(): + return repository.clear() diff --git a/src/vidxp/infrastructure/local_media.py b/src/vidxp/infrastructure/local_media.py new file mode 100644 index 0000000..ecbc35b --- /dev/null +++ b/src/vidxp/infrastructure/local_media.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +import hashlib +import json +import math +import mimetypes +import os +import subprocess +import tempfile +from contextlib import AbstractContextManager +from pathlib import Path +from typing import Any +from filelock import FileLock + +from vidxp.core.media import ( + InvalidMediaError, + MediaProbeUnavailableError, + MediaProbe, + MediaImportLimitError, + MediaStream, + MediaStoreIntegrityError, + safe_media_suffix, + StagedMedia, + StoredMedia, +) +from vidxp.infrastructure.local_files import ( + prepare_managed_destination, +) +from vidxp.infrastructure.local_objects import LocalObjectStore + + +class LocalMediaStore: + def __init__(self, root: Path, *, max_bytes: int) -> None: + self.root = root + self.objects = root / "objects" + self.staging = root / ".staging" + self.max_bytes = max_bytes + self._managed = LocalObjectStore(root) + + def stage_local(self, path: Path) -> StagedMedia: + source = path.resolve(strict=True) + if not source.is_file(): + raise FileNotFoundError("The local media source is not a file.") + initial = source.stat() + if initial.st_size > self.max_bytes: + raise MediaImportLimitError( + "The media exceeds the configured import limit." + ) + + prepare_managed_destination( + self.root, + ".staging/.media-placeholder", + ) + temporary: Path | None = None + digest = hashlib.sha256() + byte_size = 0 + try: + with source.open("rb") as reader: + opened_before = os.fstat(reader.fileno()) + with tempfile.NamedTemporaryFile( + mode="w+b", + dir=self.staging, + prefix=".media.", + suffix=".tmp", + delete=False, + ) as writer: + temporary = Path(writer.name) + while chunk := reader.read(1024 * 1024): + byte_size += len(chunk) + if byte_size > self.max_bytes: + raise MediaImportLimitError( + "The media exceeds the configured import limit." + ) + digest.update(chunk) + writer.write(chunk) + writer.flush() + os.fsync(writer.fileno()) + opened_after = os.fstat(reader.fileno()) + + current = source.stat() + if ( + opened_before.st_size != opened_after.st_size + or opened_before.st_mtime_ns != opened_after.st_mtime_ns + or opened_after.st_size != current.st_size + or opened_after.st_mtime_ns != current.st_mtime_ns + or ( + opened_after.st_ino + and current.st_ino + and opened_after.st_ino != current.st_ino + ) + ): + raise MediaStoreIntegrityError( + "The media changed while it was being imported." + ) + + checksum = digest.hexdigest() + if byte_size == 0: + raise InvalidMediaError("The media file is empty.") + suffix = safe_media_suffix(source) + storage_key = ( + f"objects/{checksum[:2]}/{checksum[2:4]}/" + f"{checksum}{suffix}" + ) + staged = StagedMedia( + sha256=checksum, + byte_size=byte_size, + storage_key=storage_key, + path=temporary, + ) + temporary = None + return staged + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + def publication_lock(self, sha256: str) -> AbstractContextManager[None]: + lock_path = prepare_managed_destination( + self.root, + f".locks/{sha256}.lock", + ) + return FileLock(lock_path) + + def publish(self, staged: StagedMedia) -> StoredMedia: + try: + path, checksum, byte_size = self._managed.publish( + staged.path, + staged.storage_key, + expected_sha256=staged.sha256, + replace_corrupt=True, + ) + except RuntimeError as exc: + raise MediaStoreIntegrityError( + "The staged media failed its integrity check." + ) from exc + return StoredMedia( + sha256=checksum, + byte_size=byte_size, + storage_key=staged.storage_key, + local_path=path, + ) + + def discard(self, staged: StagedMedia) -> None: + staged.path.unlink(missing_ok=True) + + def delete(self, storage_key: str) -> None: + self._managed.delete(storage_key) + + def verify( + self, + storage_key: str, + *, + sha256: str, + byte_size: int, + ) -> Path: + try: + return self._managed.verify( + storage_key, + sha256=sha256, + byte_size=byte_size, + ) + except RuntimeError as exc: + raise MediaStoreIntegrityError( + "Managed media failed its integrity check." + ) from exc + + def resolve(self, storage_key: str) -> Path: + self.objects.mkdir(parents=True, exist_ok=True) + return self._managed.resolve(storage_key) + + +class FFprobeMediaProbe: + _MIME_TYPES = { + "avi": "video/x-msvideo", + "matroska": "video/x-matroska", + "mov": "video/quicktime", + "mp4": "video/mp4", + "mpeg": "video/mpeg", + "webm": "video/webm", + } + + def __init__( + self, + executable: str = "ffprobe", + *, + timeout_seconds: float = 30.0, + ) -> None: + self.executable = executable + self.timeout_seconds = timeout_seconds + + def probe(self, path: Path) -> MediaProbe: + output = self._output(path) + try: + payload = json.loads(output) + if not isinstance(payload, dict): + raise TypeError("ffprobe root must be an object") + stream_items = payload.get("streams", ()) + if not isinstance(stream_items, list): + raise TypeError("ffprobe streams must be a list") + streams = tuple( + self._stream(item) + for item in stream_items + if isinstance(item, dict) + ) + if len(streams) != len(stream_items): + raise TypeError("ffprobe stream entries must be objects") + format_data = payload["format"] + if not isinstance(format_data, dict): + raise TypeError("ffprobe format must be an object") + formats = tuple( + part.strip() + for part in str(format_data["format_name"]).split(",") + if part.strip() + ) + duration = float(format_data["duration"]) + except ( + AttributeError, + KeyError, + OverflowError, + TypeError, + UnicodeDecodeError, + ValueError, + json.JSONDecodeError, + ) as exc: + raise InvalidMediaError("The media probe result is invalid.") from exc + if ( + not formats + or not math.isfinite(duration) + or duration <= 0 + or len(streams) > 128 + or not any(stream.kind == "video" for stream in streams) + ): + raise InvalidMediaError("The file is not a valid supported video.") + container = self._preferred_container(formats, path) + return MediaProbe( + detected_mime_type=self._MIME_TYPES.get( + container, + mimetypes.guess_type(path.name)[0] + or "application/octet-stream", + ), + container=container, + duration_seconds=duration, + streams=streams, + ) + + def _output(self, path: Path) -> bytes: + try: + with ( + tempfile.TemporaryFile() as stdout, + tempfile.TemporaryFile() as stderr, + ): + subprocess.run( + [ + self.executable, + "-v", + "error", + "-protocol_whitelist", + "file,pipe", + "-show_entries", + ( + "format=format_name,duration:" + "stream=index,codec_type,codec_name,width,height," + "channels,sample_rate" + ), + "-of", + "json", + str(path), + ], + check=True, + stdout=stdout, + stderr=stderr, + timeout=self.timeout_seconds, + ) + stdout.seek(0, os.SEEK_END) + if stdout.tell() > 1024 * 1024: + raise InvalidMediaError( + "The media probe result is unexpectedly large." + ) + stdout.seek(0) + return stdout.read() + except OSError as exc: + raise MediaProbeUnavailableError( + "The configured ffprobe executable is unavailable." + ) from exc + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: + raise InvalidMediaError( + "The media could not be validated by ffprobe." + ) from exc + + @staticmethod + def _stream(item: dict[str, Any]) -> MediaStream: + def optional_int(name: str) -> int | None: + value = item.get(name) + return None if value in (None, "", 0, "0") else int(value) + + return MediaStream( + index=int(item["index"]), + kind=str(item["codec_type"]), + codec=str(item["codec_name"]), + width=optional_int("width"), + height=optional_int("height"), + channels=optional_int("channels"), + sample_rate=optional_int("sample_rate"), + ) + + @staticmethod + def _preferred_container( + formats: tuple[str, ...], + path: Path, + ) -> str: + if "matroska" in formats and "webm" in formats: + return "webm" if path.suffix.lower() == ".webm" else "matroska" + for candidate in ("matroska", "webm", "mp4", "mov", "avi", "mpeg"): + if candidate in formats: + return candidate + return formats[0] diff --git a/src/vidxp/infrastructure/local_objects.py b/src/vidxp/infrastructure/local_objects.py new file mode 100644 index 0000000..7c7bf24 --- /dev/null +++ b/src/vidxp/infrastructure/local_objects.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import os +from pathlib import Path + +from vidxp.core.manifest import sha256_file +from vidxp.infrastructure.local_files import ( + durable_replace, + durable_unlink, + prepare_managed_destination, + resolve_managed_file, +) + + +class LocalObjectStore: + """Shared confined-file lifecycle for local media and artifacts.""" + + def __init__(self, root: Path) -> None: + self.root = root + + def publish( + self, + source: Path, + storage_key: str, + *, + expected_sha256: str | None, + replace_corrupt: bool, + ) -> tuple[Path, str, int]: + if not source.is_file() or source.stat().st_size <= 0: + raise FileNotFoundError("The staged object was not created.") + with source.open("r+b") as handle: + os.fsync(handle.fileno()) + checksum = sha256_file(source) + if expected_sha256 is not None and checksum != expected_sha256: + raise RuntimeError("The staged object checksum changed.") + + destination = prepare_managed_destination(self.root, storage_key) + created = not destination.exists() + published = False + try: + if destination.exists(): + if sha256_file(destination) == checksum: + durable_unlink(source) + elif replace_corrupt: + durable_replace(source, destination) + published = True + else: + raise FileExistsError( + "The managed object destination already exists." + ) + else: + durable_replace(source, destination) + published = True + resolved = destination.resolve(strict=True) + byte_size = resolved.stat().st_size + return resolved, checksum, byte_size + except BaseException: + if created and published: + durable_unlink(destination, missing_ok=True) + raise + + def verify( + self, + storage_key: str, + *, + sha256: str, + byte_size: int, + ) -> Path: + path = self.resolve(storage_key) + if path.stat().st_size != byte_size: + raise RuntimeError("Managed object has an unexpected byte size.") + if sha256_file(path) != sha256: + raise RuntimeError("Managed object has an unexpected checksum.") + return path + + def delete(self, storage_key: str) -> None: + durable_unlink(self.resolve(storage_key)) + + def resolve(self, storage_key: str) -> Path: + return resolve_managed_file(self.root, storage_key) diff --git a/src/vidxp/infrastructure/local_snapshots.py b/src/vidxp/infrastructure/local_snapshots.py new file mode 100644 index 0000000..e364f27 --- /dev/null +++ b/src/vidxp/infrastructure/local_snapshots.py @@ -0,0 +1,482 @@ +from __future__ import annotations + +from contextlib import contextmanager +from dataclasses import asdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Iterator, Mapping +from uuid import uuid4 + +from filelock import FileLock, Timeout +from pydantic import ValidationError + +from vidxp.core.contracts import ( + IndexConfig, + IndexSchemaError, +) +from vidxp.core.manifest import MANIFEST_FILE, sha256_file, write_json_atomic +from vidxp.core.generations import CompletedGenerationManifest +from vidxp.core.snapshots import ( + ActiveSnapshotPointer, + GenerationReference, + IndexSnapshot, +) +from vidxp.index_state import ( + IndexNotReadyError, + IndexingInProgressError, + snapshot_status, +) +from vidxp.repository_layout import RepositoryLayout + + +def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + +class LocalSnapshotRepository: + """Durable snapshot metadata for one local index repository. + + The repository is intended for a local filesystem with working advisory + file locks and atomic same-filesystem replacement semantics. + """ + + def __init__(self, indexes: str | Path): + index_path = Path(indexes) + self.layout = RepositoryLayout(root=index_path.parent) + if index_path != self.layout.indexes: + raise ValueError( + "LocalSnapshotRepository requires a RepositoryLayout indexes " + "directory." + ) + + @property + def indexes(self) -> Path: + return self.layout.indexes + + @property + def store(self) -> Path: + return self.layout.index_store + + @property + def generations(self) -> Path: + return self.layout.generations + + @property + def snapshots(self) -> Path: + return self.layout.snapshots + + @property + def active_pointer(self) -> Path: + return self.layout.active_snapshot + + @property + def lease_path(self) -> Path: + return self.layout.index_lease + + def ensure_directories(self) -> None: + for path in ( + self.indexes, + self.store, + self.generations, + self.snapshots, + ): + path.mkdir(parents=True, exist_ok=True) + + @contextmanager + def lease(self) -> Iterator[None]: + self.ensure_directories() + lock = FileLock(self.lease_path) + try: + lock.acquire(timeout=0) + except Timeout as exc: + raise IndexingInProgressError( + f"An index mutation is already active for {self.indexes}." + ) from exc + try: + yield + finally: + lock.release() + + def mutation_in_progress(self) -> bool: + self.ensure_directories() + lock = FileLock(self.lease_path) + try: + lock.acquire(timeout=0) + except Timeout: + return True + lock.release() + return False + + def new_generation_id(self) -> str: + return uuid4().hex + + def generation_directory(self, generation_id: str) -> Path: + return self.generations / generation_id + + def _snapshot_path(self, snapshot_id: str) -> Path: + return self.snapshots / f"{snapshot_id}.json" + + def read_active(self, *, required: bool = False) -> IndexSnapshot | None: + resolved = self._read_active(required=required) + return None if resolved is None else resolved[1] + + def _read_active( + self, + *, + required: bool = False, + ) -> tuple[ActiveSnapshotPointer, IndexSnapshot] | None: + if not self.active_pointer.is_file(): + if required: + raise IndexNotReadyError( + "No active index snapshot was found. Index media first." + ) + return None + try: + pointer = ActiveSnapshotPointer.model_validate_json( + self.active_pointer.read_text(encoding="utf-8") + ) + snapshot = self.read_snapshot( + pointer.snapshot_id, + expected_sha256=pointer.snapshot_sha256, + ) + return pointer, snapshot + except IndexSchemaError: + raise + except (OSError, ValueError, ValidationError) as exc: + raise IndexSchemaError( + "The active snapshot metadata is invalid." + ) from exc + + def read_snapshot( + self, + snapshot_id: str, + *, + expected_sha256: str | None = None, + ) -> IndexSnapshot: + snapshot_path = self._snapshot_path(snapshot_id) + if not snapshot_path.is_file(): + raise IndexSchemaError( + f"Index snapshot {snapshot_id} is missing." + ) + if ( + expected_sha256 is not None + and sha256_file(snapshot_path) != expected_sha256 + ): + raise IndexSchemaError( + f"Index snapshot {snapshot_id} failed integrity validation." + ) + try: + snapshot = IndexSnapshot.model_validate_json( + snapshot_path.read_text(encoding="utf-8") + ) + except (OSError, ValueError, ValidationError) as exc: + raise IndexSchemaError( + f"Index snapshot {snapshot_id} is invalid." + ) from exc + if snapshot.snapshot_id != snapshot_id: + raise IndexSchemaError( + "The snapshot filename and document identifier differ." + ) + self._validate_generations(snapshot) + return snapshot + + def _validate_generations(self, snapshot: IndexSnapshot) -> None: + for media_id, reference in snapshot.generations.items(): + if reference.config_fingerprint != snapshot.config_fingerprint: + raise IndexSchemaError( + f"Generation metadata for {media_id!r} has a " + "different index profile." + ) + self.validate_generation(reference) + + def validate_generation( + self, + reference: GenerationReference, + ) -> CompletedGenerationManifest: + manifest_path = ( + self.generation_directory(reference.generation_id) / MANIFEST_FILE + ) + if not manifest_path.is_file(): + raise IndexSchemaError( + f"Manifest for generation {reference.generation_id} is missing." + ) + if sha256_file(manifest_path) != reference.manifest_sha256: + raise IndexSchemaError( + f"Manifest for generation {reference.generation_id} " + "failed integrity validation." + ) + try: + manifest = CompletedGenerationManifest.model_validate_json( + manifest_path.read_text(encoding="utf-8") + ) + except (OSError, ValueError, ValidationError) as exc: + raise IndexSchemaError( + f"Manifest for generation {reference.generation_id} is invalid." + ) from exc + if ( + manifest.generation_id != reference.generation_id + or manifest.config_fingerprint != reference.config_fingerprint + ): + raise IndexSchemaError( + f"Manifest for generation {reference.generation_id} " + "does not describe a completed compatible generation." + ) + source = manifest.inputs.get(reference.media_id) + if source is None or source.sha256 != reference.input_sha256: + raise IndexSchemaError( + f"Input checksum for generation {reference.generation_id} " + "does not match its snapshot reference." + ) + if ( + manifest.store_size_bytes_at_commit + != reference.store_size_bytes_at_commit + ): + raise IndexSchemaError( + f"Index size for generation {reference.generation_id} " + "does not match its snapshot reference." + ) + if set(manifest.completed_videos) != {reference.media_id}: + raise IndexSchemaError( + f"Generation {reference.generation_id} is not a complete " + "single-media generation." + ) + if tuple(manifest.configuration["enabled_modalities"]) != ( + reference.modalities + ) or dict(manifest.record_counts) != dict(reference.record_counts): + raise IndexSchemaError( + f"Modalities for generation {reference.generation_id} " + "do not match its snapshot reference." + ) + return manifest + + def generation_reference( + self, + *, + generation_id: str, + media_id: str, + ) -> GenerationReference: + manifest_path = self.generation_directory(generation_id) / MANIFEST_FILE + try: + manifest = CompletedGenerationManifest.model_validate_json( + manifest_path.read_text(encoding="utf-8") + ) + reference = GenerationReference( + generation_id=generation_id, + media_id=media_id, + manifest_sha256=sha256_file(manifest_path), + input_sha256=manifest.inputs[media_id].sha256, + config_fingerprint=manifest.config_fingerprint, + modalities=tuple( + manifest.configuration["enabled_modalities"] + ), + record_counts=dict(manifest.record_counts), + store_size_bytes_at_commit=( + manifest.store_size_bytes_at_commit + ), + ) + except (OSError, KeyError, TypeError, ValueError, ValidationError) as exc: + raise IndexSchemaError( + f"Completed generation {generation_id} has an invalid manifest." + ) from exc + self.validate_generation(reference) + return reference + + def publish_generation( + self, + reference: GenerationReference, + config: IndexConfig, + ) -> IndexSnapshot: + active = self.require_compatible_profile( + media_id=reference.media_id, + config_fingerprint=reference.config_fingerprint, + ) + generations = dict(active.generations) if active is not None else {} + other_media = set(generations) - {reference.media_id} + generations[reference.media_id] = reference + configuration = ( + dict(active.configuration) + if active is not None and other_media + else self.snapshot_configuration(config) + ) + return self._publish( + generations=generations, + config_fingerprint=reference.config_fingerprint, + configuration=configuration, + ) + + def require_compatible_profile( + self, + *, + media_id: str, + config_fingerprint: str, + ) -> IndexSnapshot | None: + active = self.read_active() + if active is None: + return None + other_media = set(active.generations) - {media_id} + if ( + other_media + and active.config_fingerprint != config_fingerprint + ): + raise IndexSchemaError( + "All media in one active snapshot must use the same index " + "profile. Re-index or clear the other media first." + ) + return active + + def remove(self, media_id: str) -> bool: + active = self.read_active() + if active is None or media_id not in active.generations: + return False + generations = dict(active.generations) + del generations[media_id] + self._publish( + generations=generations, + config_fingerprint=active.config_fingerprint, + configuration=dict(active.configuration), + ) + return True + + def clear(self) -> bool: + active = self.read_active() + if active is None or not active.generations: + return False + self._publish( + generations={}, + config_fingerprint=active.config_fingerprint, + configuration=dict(active.configuration), + ) + return True + + def _publish( + self, + *, + generations: Mapping[str, GenerationReference], + config_fingerprint: str, + configuration: dict[str, Any], + ) -> IndexSnapshot: + snapshot = IndexSnapshot( + snapshot_id=uuid4().hex, + created_at=_utc_now(), + config_fingerprint=config_fingerprint, + configuration=configuration, + generations=dict(generations), + ) + snapshot_path = self._snapshot_path(snapshot.snapshot_id) + if snapshot_path.exists(): + raise FileExistsError( + f"Snapshot {snapshot.snapshot_id} already exists." + ) + write_json_atomic( + snapshot_path, + snapshot.model_dump(mode="json"), + ) + pointer = ActiveSnapshotPointer( + snapshot_id=snapshot.snapshot_id, + snapshot_sha256=sha256_file(snapshot_path), + updated_at=_utc_now(), + ) + write_json_atomic( + self.active_pointer, + pointer.model_dump(mode="json"), + ) + return snapshot + + @staticmethod + def snapshot_configuration(config: IndexConfig) -> dict[str, Any]: + payload = asdict(config) + for key in ( + "video_id", + "output_root", + "storage_directory", + "generation_directory", + "generation_id", + "snapshot_id", + "snapshot_sha256", + ): + payload.pop(key, None) + payload["enabled_modalities"] = list(config.enabled_modalities) + payload["collection_names"] = dict(config.collection_names) + payload["capability_options"] = { + name: dict(values) + for name, values in config.capability_options.items() + } + return payload + + def active_config( + self, + *, + device: str, + ) -> tuple[IndexConfig, IndexSnapshot]: + resolved = self._read_active(required=True) + assert resolved is not None + pointer, snapshot = resolved + if not snapshot.generations: + raise IndexNotReadyError( + "The active index snapshot contains no media." + ) + return ( + self._config_for_snapshot( + snapshot, + snapshot_sha256=pointer.snapshot_sha256, + device=device, + ), + snapshot, + ) + + def config_for_snapshot( + self, + snapshot_id: str, + *, + snapshot_sha256: str, + device: str, + ) -> IndexConfig: + snapshot = self.read_snapshot( + snapshot_id, + expected_sha256=snapshot_sha256, + ) + if not snapshot.generations: + raise IndexNotReadyError( + "The requested index snapshot contains no media." + ) + return self._config_for_snapshot( + snapshot, + snapshot_sha256=snapshot_sha256, + device=device, + ) + + def _config_for_snapshot( + self, + snapshot: IndexSnapshot, + *, + snapshot_sha256: str, + device: str, + ) -> IndexConfig: + stored = dict(snapshot.configuration) + stored["enabled_modalities"] = tuple(stored["enabled_modalities"]) + stored["collection_names"] = dict(stored["collection_names"]) + stored.update( + { + "storage_directory": str(self.store), + "snapshot_id": snapshot.snapshot_id, + "snapshot_sha256": snapshot_sha256, + "device": device, + } + ) + try: + config = IndexConfig(**stored) + except (TypeError, ValueError) as exc: + raise IndexSchemaError( + "The snapshot configuration is invalid." + ) from exc + if config.fingerprint() != snapshot.config_fingerprint: + raise IndexSchemaError( + "The snapshot configuration fingerprint is invalid." + ) + return config + + def status(self) -> dict[str, Any] | None: + resolved = self._read_active() + if resolved is None: + return None + return snapshot_status(resolved[1]) diff --git a/src/vidxp/infrastructure/local_worker.py b/src/vidxp/infrastructure/local_worker.py new file mode 100644 index 0000000..08626b7 --- /dev/null +++ b/src/vidxp/infrastructure/local_worker.py @@ -0,0 +1,331 @@ +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path +from secrets import token_hex +from threading import Lock +from time import monotonic, sleep + +from filelock import FileLock, Timeout +from pydantic import ValidationError + +from vidxp.core.manifest import write_json_atomic +from vidxp.infrastructure.local_files import durable_unlink +from vidxp.settings import VidXPSettings +from vidxp.workflow_runtime import ( + LOCAL_WORKER_BOOTSTRAP_ENV, + LocalWorkerBootstrap, + LocalWorkerReady, + LocalWorkerStopRequest, + local_worker_bootstrap, + local_executor_id, + workflow_application_version, +) + + +class LocalWorkerSupervisor: + """Start one detached repository-scoped worker without owning job state.""" + + _retry_delay_seconds = 5.0 + _startup_timeout_seconds = 15.0 + + def __init__(self, settings: VidXPSettings) -> None: + self.settings = settings + self.layout = settings.layout + self._startup_lock = Lock() + self._retry_after = 0.0 + + def ensure_running(self) -> None: + with self._startup_lock: + if monotonic() < self._retry_after: + raise RuntimeError( + "The local background worker is in startup backoff." + ) + try: + self._ensure_running() + except Exception: + self._retry_after = ( + monotonic() + self._retry_delay_seconds + ) + raise + self._retry_after = 0.0 + + def _ensure_running(self) -> None: + self.layout.ensure_local_directories() + version = workflow_application_version() + bootstrap = local_worker_bootstrap(self.settings) + worker_lock_path = ( + self.layout.local_workflows / f"worker-{version}.lock" + ) + ready_path = ( + self.layout.local_workflows / f"worker-{version}.ready" + ) + stop_path = ( + self.layout.local_workflows / f"worker-{version}.stop" + ) + start_lock = FileLock( + self.layout.local_workflows / f"worker-{version}-start.lock" + ) + with start_lock: + self._remove_stale_bootstraps(version) + worker_lock = FileLock(worker_lock_path) + if self._wait_for_existing_worker( + worker_lock, + ready_path, + fingerprint=bootstrap.fingerprint, + ): + return + durable_unlink(ready_path, missing_ok=True) + durable_unlink(stop_path, missing_ok=True) + + command = [ + sys.executable, + "-m", + "vidxp.workflow_worker", + "--executor-id", + local_executor_id(self.settings), + "--role", + "all", + "--lock-file", + str(worker_lock_path.resolve()), + "--ready-file", + str(ready_path.resolve()), + "--stop-file", + str(stop_path.resolve()), + "--log-file", + str(self._worker_log(version).resolve()), + ] + bootstrap_path = self._write_bootstrap(bootstrap) + environment = { + key: value + for key, value in os.environ.items() + if not key.upper().startswith(("VIDXP_", "DBOS_")) + } + environment[LOCAL_WORKER_BOOTSTRAP_ENV] = str( + bootstrap_path.resolve() + ) + options: dict = { + "stdin": subprocess.DEVNULL, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL, + "close_fds": True, + "env": environment, + } + if sys.platform == "win32": + options["creationflags"] = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | subprocess.CREATE_NO_WINDOW + ) + else: + options["start_new_session"] = True + process = None + try: + process = subprocess.Popen( + command, + **options, + ) + self._wait_for_worker( + process, + worker_lock, + ready_path, + fingerprint=bootstrap.fingerprint, + timeout_seconds=self._startup_timeout_seconds, + ) + except Exception: + if process is not None: + self._terminate(process) + raise + finally: + durable_unlink(bootstrap_path, missing_ok=True) + + def _worker_log(self, version: str) -> Path: + return self.layout.local_workflows / f"worker-{version}.log" + + def _write_bootstrap( + self, + bootstrap: LocalWorkerBootstrap, + ) -> Path: + path = ( + self.layout.local_workflows + / ( + f".worker-{workflow_application_version()}-" + f"bootstrap-{token_hex(16)}.json" + ) + ) + descriptor = os.open( + path, + os.O_WRONLY | os.O_CREAT | os.O_EXCL, + 0o600, + ) + try: + with os.fdopen(descriptor, "w", encoding="utf-8") as destination: + destination.write(bootstrap.model_dump_json()) + destination.flush() + os.fsync(destination.fileno()) + except BaseException: + durable_unlink(path, missing_ok=True) + raise + return path + + def _remove_stale_bootstraps(self, version: str) -> None: + pattern = f".worker-{version}-bootstrap-*.json" + for path in self.layout.local_workflows.glob(pattern): + if path.is_file(): + durable_unlink(path) + + def health(self) -> None: + version = workflow_application_version() + worker_lock = FileLock( + self.layout.local_workflows / f"worker-{version}.lock" + ) + ready_path = ( + self.layout.local_workflows / f"worker-{version}.ready" + ) + fingerprint = local_worker_bootstrap(self.settings).fingerprint + if not self._wait_for_existing_worker( + worker_lock, + ready_path, + fingerprint=fingerprint, + timeout_seconds=0.1, + ): + raise RuntimeError("The local background worker is not running.") + + def stop(self) -> bool: + version = workflow_application_version() + worker_lock = FileLock( + self.layout.local_workflows / f"worker-{version}.lock" + ) + ready_path = ( + self.layout.local_workflows / f"worker-{version}.ready" + ) + stop_path = ( + self.layout.local_workflows / f"worker-{version}.stop" + ) + try: + worker_lock.acquire(timeout=0) + except Timeout: + ready = self._load_ready(ready_path) + if ready.application_version != version: + raise RuntimeError( + "The running local worker has an invalid version identity." + ) + write_json_atomic( + stop_path, + LocalWorkerStopRequest( + pid=ready.pid, + application_version=version, + ).model_dump(mode="json"), + ) + deadline = monotonic() + 35 + while monotonic() < deadline: + try: + worker_lock.acquire(timeout=0) + except Timeout: + sleep(0.05) + continue + else: + worker_lock.release() + durable_unlink(ready_path, missing_ok=True) + durable_unlink(stop_path, missing_ok=True) + return True + raise RuntimeError( + "The local background worker did not stop in time." + ) + else: + worker_lock.release() + durable_unlink(ready_path, missing_ok=True) + durable_unlink(stop_path, missing_ok=True) + return False + + @staticmethod + def _wait_for_existing_worker( + worker_lock: FileLock, + ready_path: Path, + *, + fingerprint: str, + timeout_seconds: float = _startup_timeout_seconds, + ) -> bool: + deadline = monotonic() + timeout_seconds + while monotonic() < deadline: + try: + worker_lock.acquire(timeout=0) + except Timeout: + if ready_path.is_file(): + LocalWorkerSupervisor._validate_ready( + ready_path, + fingerprint, + ) + return True + sleep(0.05) + continue + else: + worker_lock.release() + return False + raise RuntimeError( + "The existing local background worker did not become ready." + ) + + @staticmethod + def _wait_for_worker( + process: subprocess.Popen, + worker_lock: FileLock, + ready_path: Path, + *, + fingerprint: str, + timeout_seconds: float = _startup_timeout_seconds, + ) -> None: + deadline = monotonic() + timeout_seconds + while monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError( + "The local background worker exited during startup." + ) + try: + worker_lock.acquire(timeout=0) + except Timeout: + if ready_path.is_file(): + LocalWorkerSupervisor._validate_ready( + ready_path, + fingerprint, + ) + return + else: + worker_lock.release() + sleep(0.05) + raise RuntimeError( + "The local background worker did not become ready in time." + ) + + @staticmethod + def _validate_ready(ready_path: Path, fingerprint: str) -> None: + ready = LocalWorkerSupervisor._load_ready(ready_path) + if ready.fingerprint != fingerprint: + raise RuntimeError( + "The running local worker uses different execution settings " + f"(PID {ready.pid}); run `vidxp jobs stop-worker` before " + "applying the new configuration." + ) + + @staticmethod + def _load_ready(ready_path: Path) -> LocalWorkerReady: + try: + return LocalWorkerReady.model_validate_json( + ready_path.read_text(encoding="utf-8") + ) + except (OSError, ValidationError) as exc: + raise RuntimeError( + "The local background worker published invalid readiness." + ) from exc + + @staticmethod + def _terminate(process: subprocess.Popen) -> None: + if process.poll() is not None: + return + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) diff --git a/src/vidxp/infrastructure/ollama_query.py b/src/vidxp/infrastructure/ollama_query.py new file mode 100644 index 0000000..9347d04 --- /dev/null +++ b/src/vidxp/infrastructure/ollama_query.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import httpx +from openai import OpenAIError +from pydantic import ValidationError +from pydantic_ai import Agent, NativeOutput +from pydantic_ai.exceptions import AgentRunError +from pydantic_ai.models.ollama import OllamaModel +from pydantic_ai.providers.ollama import OllamaProvider + +from vidxp.application_models import ( + DraftAnswer, + QueryModelIdentity, + QueryPlan, + QueryPlanningRequest, + QuerySynthesisRequest, +) +from vidxp.ports import QueryProviderError + + +_PLANNING_INSTRUCTIONS = """ +Return exactly one search_moments step for every allowed modality, using a +short retrieval query derived from the question. Include actor_overview +exactly once when actor_overview_allowed is true. Do not invent modalities, +filters, identifiers, limits, paths, model names, or operations. +""".strip() + +_SYNTHESIS_INSTRUCTIONS = """ +Write concise factual claims supported only by the supplied evidence. Every +claim must cite one or more evidence_id values exactly as supplied. Do not +infer visual facts from similarity scores, introduce outside knowledge, or +mention evidence that was not supplied. +""".strip() + + +class OllamaQueryModel: + """Structured-output adapter for a configured self-hosted Ollama server.""" + + def __init__( + self, + *, + base_url: str, + model_name: str, + timeout_seconds: float, + output_retries: int, + http_client: httpx.AsyncClient | None = None, + ) -> None: + model = OllamaModel( + model_name, + provider=OllamaProvider( + base_url=base_url, + http_client=http_client, + ), + ) + retries = {"output": output_retries} + self._identity = QueryModelIdentity( + provider="ollama", + model=model_name, + ) + self._planner = Agent( + model, + output_type=NativeOutput(QueryPlan), + instructions=_PLANNING_INSTRUCTIONS, + retries=retries, + model_settings={ + "temperature": 0, + "max_tokens": 1024, + "timeout": timeout_seconds, + }, + ) + self._synthesizer = Agent( + model, + output_type=NativeOutput(DraftAnswer), + instructions=_SYNTHESIS_INSTRUCTIONS, + retries=retries, + model_settings={ + "temperature": 0, + "max_tokens": 2048, + "timeout": timeout_seconds, + }, + ) + + @property + def identity(self) -> QueryModelIdentity: + return self._identity + + def plan(self, request: QueryPlanningRequest) -> QueryPlan: + return self._run(self._planner, request.model_dump_json()) + + def synthesize(self, request: QuerySynthesisRequest) -> DraftAnswer: + return self._run(self._synthesizer, request.model_dump_json()) + + @staticmethod + def _run(agent: Agent, prompt: str): + try: + return agent.run_sync(prompt).output + except ( + AgentRunError, + OpenAIError, + httpx.HTTPError, + TimeoutError, + ValidationError, + ) as exc: + raise QueryProviderError( + "The configured Ollama query model is unavailable." + ) from exc diff --git a/src/vidxp/infrastructure/sql_catalog.py b/src/vidxp/infrastructure/sql_catalog.py new file mode 100644 index 0000000..695505a --- /dev/null +++ b/src/vidxp/infrastructure/sql_catalog.py @@ -0,0 +1,698 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from datetime import datetime +from typing import Any + +from sqlalchemy import Engine, create_engine, delete, event, func, insert, select, update +from sqlalchemy.engine import Connection +from sqlalchemy.exc import IntegrityError +from sqlalchemy.pool import NullPool + +from vidxp.core.artifacts import ArtifactRecord, ArtifactState +from vidxp.core.media import MediaRecord, utc_now +from vidxp.core.uploads import UploadIntentRecord, UploadState +from vidxp.infrastructure.sql_tables import ( + artifact_requests, + artifacts, + media, + media_import_requests, + metadata, + upload_intents, + upload_quota, +) + +_RESERVED_UPLOAD_STATES = { + UploadState.pending.value, + UploadState.accepted.value, + UploadState.processing.value, + UploadState.failed.value, +} +_UPLOAD_QUOTA_ID = "1" + + +class UploadQuotaExceededError(RuntimeError): + """Raised when an atomic upload reservation exceeds its quota.""" + + +def _payload(value: Any) -> dict[str, Any]: + if not isinstance(value, dict): + raise RuntimeError("The relational catalog payload is invalid.") + return value + + +def _record(model: Any, value: Any) -> Any: + return model.model_validate(_payload(value), strict=False) + + +def _upload_record(row: Any) -> UploadIntentRecord: + return UploadIntentRecord( + intent_id=row.intent_id, + request_key=row.request_key, + original_filename=row.original_filename, + byte_size=row.byte_size, + declared_mime_type=row.declared_mime_type, + state=row.state, + created_at=datetime.fromisoformat(row.created_at), + expires_at=datetime.fromisoformat(row.expires_at), + upload_id=row.upload_id, + job_id=row.job_id, + media_id=row.media_id, + ) + + +class SQLCatalog: + """SQLAlchemy Core catalog shared by SQLite and PostgreSQL adapters.""" + + def __init__( + self, + database_url: str, + *, + initialize: bool = False, + engine: Engine | None = None, + ) -> None: + sqlite = database_url.startswith("sqlite:") + self.engine = engine or create_engine( + database_url, + pool_pre_ping=True, + **( + { + "poolclass": NullPool, + "connect_args": {"timeout": 30}, + } + if sqlite + else {} + ), + ) + self._owns_engine = engine is None + if sqlite and engine is None: + event.listen(self.engine, "connect", self._configure_sqlite) + if initialize: + metadata.create_all(self.engine) + + def close(self) -> None: + if self._owns_engine: + self.engine.dispose() + + def health(self) -> None: + with self.engine.connect() as connection: + connection.execute(select(1)).scalar_one() + + @contextmanager + def transaction(self) -> Iterator[Connection]: + with self._write_transaction() as connection: + yield connection + + @staticmethod + def _configure_sqlite(dbapi_connection: Any, _record: Any) -> None: + cursor = dbapi_connection.cursor() + try: + cursor.execute("PRAGMA foreign_keys = ON") + cursor.execute("PRAGMA busy_timeout = 30000") + finally: + cursor.close() + + @contextmanager + def _write_transaction(self) -> Iterator[Connection]: + with self.engine.connect() as connection: + if self.engine.dialect.name == "sqlite": + connection.exec_driver_sql("BEGIN IMMEDIATE") + try: + yield connection + except BaseException: + connection.rollback() + raise + else: + connection.commit() + else: + with connection.begin(): + yield connection + + def get_media(self, media_id: str) -> MediaRecord | None: + with self.engine.connect() as connection: + payload = connection.execute( + select(media.c.payload).where(media.c.media_id == media_id) + ).scalar_one_or_none() + return ( + None + if payload is None + else _record(MediaRecord, payload) + ) + + def get_media_by_checksum(self, sha256: str) -> MediaRecord | None: + with self.engine.connect() as connection: + payload = connection.execute( + select(media.c.payload).where(media.c.sha256 == sha256) + ).scalar_one_or_none() + return ( + None + if payload is None + else _record(MediaRecord, payload) + ) + + def put_media(self, record: MediaRecord) -> MediaRecord: + values = { + "media_id": record.media_id, + "sha256": record.sha256, + "created_at": record.created_at.isoformat(), + "payload": record.model_dump(mode="json"), + } + with self._write_transaction() as connection: + existing = self._media_by_id(connection, record.media_id) + if existing is not None: + if existing != record: + raise FileExistsError( + f"Media {record.media_id} already has another record." + ) + return existing + checksum_match = self._media_by_checksum(connection, record.sha256) + if checksum_match is not None: + return checksum_match + try: + with connection.begin_nested(): + connection.execute(insert(media).values(**values)) + except IntegrityError: + authoritative = self._media_by_checksum( + connection, + record.sha256, + ) + if authoritative is not None: + return authoritative + existing = self._media_by_id(connection, record.media_id) + if existing == record: + return existing + raise + return record + + @staticmethod + def _media_by_id( + connection: Connection, + media_id: str, + ) -> MediaRecord | None: + payload = connection.execute( + select(media.c.payload).where(media.c.media_id == media_id) + ).scalar_one_or_none() + return ( + None + if payload is None + else _record(MediaRecord, payload) + ) + + @staticmethod + def _media_by_checksum( + connection: Connection, + sha256: str, + ) -> MediaRecord | None: + payload = connection.execute( + select(media.c.payload).where(media.c.sha256 == sha256) + ).scalar_one_or_none() + return ( + None + if payload is None + else _record(MediaRecord, payload) + ) + + def list_media( + self, + *, + limit: int, + offset: int = 0, + ) -> tuple[MediaRecord, ...]: + if limit <= 0 or offset < 0: + raise ValueError("limit must be positive and offset nonnegative") + with self.engine.connect() as connection: + payloads = connection.execute( + select(media.c.payload) + .order_by(media.c.created_at, media.c.media_id) + .limit(limit) + .offset(offset) + ).scalars() + return tuple( + _record(MediaRecord, payload) + for payload in payloads + ) + + def count_media(self) -> int: + with self.engine.connect() as connection: + return int( + connection.execute( + select(func.count()).select_from(media) + ).scalar_one() + ) + + def reserve_media_import( + self, + request_key: str, + request_fingerprint: str, + ) -> MediaRecord | None: + with self._write_transaction() as connection: + row = connection.execute( + select( + media_import_requests.c.request_fingerprint, + media_import_requests.c.media_id, + ) + .where(media_import_requests.c.request_key == request_key) + .with_for_update() + ).one_or_none() + if row is None: + try: + with connection.begin_nested(): + connection.execute( + insert(media_import_requests).values( + request_key=request_key, + request_fingerprint=request_fingerprint, + ) + ) + return None + except IntegrityError: + row = connection.execute( + select( + media_import_requests.c.request_fingerprint, + media_import_requests.c.media_id, + ) + .where( + media_import_requests.c.request_key == request_key + ) + .with_for_update() + ).one() + if row.request_fingerprint != request_fingerprint: + raise FileExistsError( + "The media import key is bound to different content." + ) + if row.media_id is None: + return None + stored = self._media_by_id(connection, row.media_id) + if stored is None: + raise RuntimeError( + "A completed media import references missing media." + ) + return stored + + def complete_media_import( + self, + request_key: str, + request_fingerprint: str, + record: MediaRecord, + ) -> None: + with self._write_transaction() as connection: + row = connection.execute( + select( + media_import_requests.c.request_fingerprint, + media_import_requests.c.media_id, + ) + .where(media_import_requests.c.request_key == request_key) + .with_for_update() + ).one_or_none() + if row is None or row.request_fingerprint != request_fingerprint: + raise FileExistsError( + "The media import reservation does not match the content." + ) + if row.media_id is not None and row.media_id != record.media_id: + raise FileExistsError( + "The media import key is already completed." + ) + connection.execute( + update(media_import_requests) + .where(media_import_requests.c.request_key == request_key) + .values(media_id=record.media_id) + ) + + def get_artifact(self, artifact_id: str) -> ArtifactRecord | None: + with self.engine.connect() as connection: + payload = connection.execute( + select(artifacts.c.payload).where( + artifacts.c.artifact_id == artifact_id + ) + ).scalar_one_or_none() + return ( + None + if payload is None + else _record(ArtifactRecord, payload) + ) + + def get_artifact_by_request( + self, + request_key: str, + ) -> ArtifactRecord | None: + with self.engine.connect() as connection: + payload = connection.execute( + select(artifacts.c.payload) + .select_from( + artifact_requests.join( + artifacts, + artifact_requests.c.artifact_id + == artifacts.c.artifact_id, + ) + ) + .where(artifact_requests.c.request_key == request_key) + ).scalar_one_or_none() + if payload is None: + return None + record = _record(ArtifactRecord, payload) + if ( + record.state != ArtifactState.ready + or ( + record.expires_at is not None + and record.expires_at <= utc_now() + ) + ): + return None + return record + + def put_artifact(self, record: ArtifactRecord) -> ArtifactRecord: + with self._write_transaction() as connection: + request_match = self._artifact_by_request( + connection, + record.request_key, + ) + if request_match is not None: + if ( + request_match.state == ArtifactState.ready + and ( + request_match.expires_at is None + or request_match.expires_at > utc_now() + ) + ): + return request_match + connection.execute( + delete(artifact_requests).where( + artifact_requests.c.request_key == record.request_key + ) + ) + existing = self._artifact_by_id( + connection, + record.artifact_id, + ) + if existing is not None: + if existing != record: + raise FileExistsError( + f"Artifact {record.artifact_id} already exists." + ) + return existing + connection.execute( + insert(artifacts).values( + artifact_id=record.artifact_id, + media_id=record.media_id, + created_at=record.created_at.isoformat(), + payload=record.model_dump(mode="json"), + ) + ) + connection.execute( + insert(artifact_requests).values( + request_key=record.request_key, + artifact_id=record.artifact_id, + ) + ) + return record + + @staticmethod + def _artifact_by_id( + connection: Connection, + artifact_id: str, + ) -> ArtifactRecord | None: + payload = connection.execute( + select(artifacts.c.payload).where( + artifacts.c.artifact_id == artifact_id + ) + ).scalar_one_or_none() + return ( + None + if payload is None + else _record(ArtifactRecord, payload) + ) + + @staticmethod + def _artifact_by_request( + connection: Connection, + request_key: str, + ) -> ArtifactRecord | None: + payload = connection.execute( + select(artifacts.c.payload) + .select_from( + artifact_requests.join( + artifacts, + artifact_requests.c.artifact_id == artifacts.c.artifact_id, + ) + ) + .where(artifact_requests.c.request_key == request_key) + ).scalar_one_or_none() + return ( + None + if payload is None + else _record(ArtifactRecord, payload) + ) + + def invalidate_artifact_request( + self, + request_key: str, + artifact_id: str, + ) -> None: + with self._write_transaction() as connection: + connection.execute( + delete(artifact_requests).where( + artifact_requests.c.request_key == request_key, + artifact_requests.c.artifact_id == artifact_id, + ) + ) + + def create_upload_intent( + self, + record: UploadIntentRecord, + *, + quota_limit: int, + ) -> UploadIntentRecord: + with self._write_transaction() as connection: + self._reserve_upload_quota( + connection, + byte_size=record.byte_size, + quota_limit=quota_limit, + ) + connection.execute( + insert(upload_intents).values( + intent_id=record.intent_id, + request_key=record.request_key, + original_filename=record.original_filename, + byte_size=record.byte_size, + declared_mime_type=record.declared_mime_type, + state=record.state.value, + created_at=record.created_at.isoformat(), + expires_at=record.expires_at.isoformat(), + upload_id=record.upload_id, + job_id=record.job_id, + media_id=record.media_id, + ) + ) + return record + + @staticmethod + def _reserve_upload_quota( + connection: Connection, + *, + byte_size: int, + quota_limit: int, + ) -> None: + row = connection.execute( + select(upload_quota.c.reserved_bytes) + .where(upload_quota.c.singleton_id == _UPLOAD_QUOTA_ID) + .with_for_update() + ).one_or_none() + if row is None: + try: + with connection.begin_nested(): + connection.execute( + insert(upload_quota).values( + singleton_id=_UPLOAD_QUOTA_ID, + reserved_bytes=0, + ) + ) + except IntegrityError: + pass + row = connection.execute( + select(upload_quota.c.reserved_bytes) + .where(upload_quota.c.singleton_id == _UPLOAD_QUOTA_ID) + .with_for_update() + ).one() + reserved = int(row.reserved_bytes) + if reserved + byte_size > quota_limit: + raise UploadQuotaExceededError + connection.execute( + update(upload_quota) + .where(upload_quota.c.singleton_id == _UPLOAD_QUOTA_ID) + .values(reserved_bytes=reserved + byte_size) + ) + + def get_upload_intent_by_request( + self, + request_key: str, + ) -> UploadIntentRecord | None: + with self.engine.connect() as connection: + row = connection.execute( + select(upload_intents).where( + upload_intents.c.request_key == request_key + ) + ).one_or_none() + return None if row is None else _upload_record(row) + + def get_upload_intent_by_upload_id( + self, + upload_id: str, + *, + connection: Connection | None = None, + for_update: bool = False, + ) -> UploadIntentRecord | None: + statement = select(upload_intents).where( + upload_intents.c.upload_id == upload_id + ) + if for_update: + statement = statement.with_for_update() + if connection is not None: + row = connection.execute(statement).one_or_none() + else: + with self.engine.connect() as active: + row = active.execute(statement).one_or_none() + return None if row is None else _upload_record(row) + + def get_upload_intent( + self, + intent_id: str, + *, + connection: Connection | None = None, + for_update: bool = False, + ) -> UploadIntentRecord | None: + statement = select(upload_intents).where( + upload_intents.c.intent_id == intent_id + ) + if for_update: + statement = statement.with_for_update() + if connection is not None: + row = connection.execute(statement).one_or_none() + else: + with self.engine.connect() as active: + row = active.execute(statement).one_or_none() + return None if row is None else _upload_record(row) + + def update_upload( + self, + intent_id: str, + *, + state: UploadState, + connection: Connection, + upload_id: str | None = None, + job_id: str | None = None, + media_id: str | None = None, + clear_upload_id: bool = False, + expected_states: set[UploadState] | None = None, + ) -> bool: + current = connection.execute( + select( + upload_intents.c.byte_size, + upload_intents.c.state, + ) + .where(upload_intents.c.intent_id == intent_id) + .with_for_update() + ).one_or_none() + if current is None: + raise RuntimeError("The upload intent update was lost.") + if ( + expected_states is not None + and UploadState(current.state) not in expected_states + ): + return False + values: dict[str, Any] = {"state": state.value} + if clear_upload_id: + values["upload_id"] = None + elif upload_id is not None: + values["upload_id"] = upload_id + if job_id is not None: + values["job_id"] = job_id + if media_id is not None: + values["media_id"] = media_id + result = connection.execute( + update(upload_intents) + .where(upload_intents.c.intent_id == intent_id) + .values(**values) + ) + if result.rowcount != 1: + raise RuntimeError("The upload intent update was lost.") + if ( + current.state in _RESERVED_UPLOAD_STATES + and state.value not in _RESERVED_UPLOAD_STATES + ): + quota = connection.execute( + select(upload_quota.c.reserved_bytes) + .where( + upload_quota.c.singleton_id == _UPLOAD_QUOTA_ID, + ) + .with_for_update() + ).scalar_one() + connection.execute( + update(upload_quota) + .where( + upload_quota.c.singleton_id == _UPLOAD_QUOTA_ID, + ) + .values( + reserved_bytes=max(0, int(quota) - current.byte_size) + ) + ) + return True + + def expired_uploads( + self, + *, + now: datetime, + ) -> tuple[UploadIntentRecord, ...]: + with self.engine.connect() as connection: + rows = connection.execute( + select(upload_intents).where( + upload_intents.c.expires_at <= now.isoformat(), + upload_intents.c.state.in_( + ( + UploadState.pending.value, + UploadState.accepted.value, + UploadState.failed.value, + ) + ), + ) + ) + return tuple(_upload_record(row) for row in rows) + + def recoverable_uploads(self) -> tuple[UploadIntentRecord, ...]: + with self.engine.connect() as connection: + rows = connection.execute( + select(upload_intents).where( + upload_intents.c.state == UploadState.accepted.value + ) + ) + return tuple(_upload_record(row) for row in rows) + + def cleanup_uploads(self) -> tuple[UploadIntentRecord, ...]: + with self.engine.connect() as connection: + rows = connection.execute( + select(upload_intents).where( + upload_intents.c.upload_id.is_not(None), + upload_intents.c.state.in_( + ( + UploadState.ready.value, + UploadState.expired.value, + ) + ), + ) + ) + return tuple(_upload_record(row) for row in rows) + + def processing_uploads(self) -> tuple[UploadIntentRecord, ...]: + with self.engine.connect() as connection: + rows = connection.execute( + select(upload_intents).where( + upload_intents.c.state == UploadState.processing.value + ) + ) + return tuple(_upload_record(row) for row in rows) + + def with_upload_transaction( + self, + operation: Callable[[Connection], Any], + ) -> Any: + with self._write_transaction() as connection: + return operation(connection) diff --git a/src/vidxp/infrastructure/sql_snapshots.py b/src/vidxp/infrastructure/sql_snapshots.py new file mode 100644 index 0000000..43a1947 --- /dev/null +++ b/src/vidxp/infrastructure/sql_snapshots.py @@ -0,0 +1,371 @@ +from __future__ import annotations + +import hashlib +import json +from contextlib import contextmanager +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Iterator +from uuid import uuid4 + +from sqlalchemy import Engine, insert, select, text, update +from sqlalchemy.engine import Connection +from sqlalchemy.exc import IntegrityError +from pydantic import ValidationError + +from vidxp.core.contracts import IndexConfig, IndexSchemaError +from vidxp.core.generations import CompletedGenerationManifest +from vidxp.core.manifest import MANIFEST_FILE, sha256_file +from vidxp.core.snapshots import GenerationReference, IndexSnapshot +from vidxp.index_state import ( + IndexNotReadyError, + IndexingInProgressError, + snapshot_status, +) +from vidxp.infrastructure.local_snapshots import LocalSnapshotRepository +from vidxp.infrastructure.sql_tables import ( + index_generations, + index_snapshots, + index_state, +) + +_INDEX_STATE_ID = "1" +_INDEX_LOCK_IDENTITY = "vidxp:index" + + +def _checksum(snapshot: IndexSnapshot) -> str: + payload = json.dumps( + snapshot.model_dump(mode="json"), + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +class SQLSnapshotRepository(LocalSnapshotRepository): + """PostgreSQL-authoritative snapshots with shared generation manifests.""" + + def __init__( + self, + indexes: str | Path, + *, + engine: Engine, + ) -> None: + super().__init__(indexes) + self.engine = engine + + @contextmanager + def lease(self) -> Iterator[None]: + self.ensure_directories() + with self.engine.connect() as connection: + acquired = connection.execute( + text("SELECT pg_try_advisory_lock(hashtext(:identity))"), + {"identity": _INDEX_LOCK_IDENTITY}, + ).scalar_one() + if not acquired: + raise IndexingInProgressError( + "An index mutation is already active for this repository." + ) + try: + yield + finally: + connection.execute( + text("SELECT pg_advisory_unlock(hashtext(:identity))"), + {"identity": _INDEX_LOCK_IDENTITY}, + ) + + def mutation_in_progress(self) -> bool: + with self.engine.connect() as connection: + acquired = connection.execute( + text("SELECT pg_try_advisory_lock(hashtext(:identity))"), + {"identity": _INDEX_LOCK_IDENTITY}, + ).scalar_one() + if acquired: + connection.execute( + text("SELECT pg_advisory_unlock(hashtext(:identity))"), + {"identity": _INDEX_LOCK_IDENTITY}, + ) + return not bool(acquired) + + @staticmethod + def _ensure_index_state(connection: Connection) -> None: + if connection.execute( + select(index_state.c.singleton_id).where( + index_state.c.singleton_id == _INDEX_STATE_ID + ) + ).scalar_one_or_none() is not None: + return + try: + with connection.begin_nested(): + connection.execute( + insert(index_state).values(singleton_id=_INDEX_STATE_ID) + ) + except IntegrityError: + pass + + def read_active(self, *, required: bool = False) -> IndexSnapshot | None: + with self.engine.connect() as connection: + row = connection.execute( + select( + index_state.c.active_snapshot_id, + index_state.c.active_snapshot_sha256, + ).where(index_state.c.singleton_id == _INDEX_STATE_ID) + ).one_or_none() + if row is None or row.active_snapshot_id is None: + if required: + raise IndexNotReadyError( + "No active index snapshot was found. Index media first." + ) + return None + return self._read_snapshot( + connection, + row.active_snapshot_id, + expected_sha256=row.active_snapshot_sha256, + ) + + def read_snapshot( + self, + snapshot_id: str, + *, + expected_sha256: str | None = None, + ) -> IndexSnapshot: + with self.engine.connect() as connection: + return self._read_snapshot( + connection, + snapshot_id, + expected_sha256=expected_sha256, + ) + + def _read_snapshot( + self, + connection: Connection, + snapshot_id: str, + *, + expected_sha256: str | None, + ) -> IndexSnapshot: + row = connection.execute( + select( + index_snapshots.c.sha256, + index_snapshots.c.payload, + ).where(index_snapshots.c.snapshot_id == snapshot_id) + ).one_or_none() + if row is None: + raise IndexSchemaError(f"Index snapshot {snapshot_id} is missing.") + if expected_sha256 is not None and row.sha256 != expected_sha256: + raise IndexSchemaError( + f"Index snapshot {snapshot_id} failed integrity validation." + ) + try: + snapshot = IndexSnapshot.model_validate(row.payload, strict=False) + except ValueError as exc: + raise IndexSchemaError( + f"Index snapshot {snapshot_id} is invalid." + ) from exc + if snapshot.snapshot_id != snapshot_id or _checksum(snapshot) != row.sha256: + raise IndexSchemaError( + f"Index snapshot {snapshot_id} failed integrity validation." + ) + self._validate_generations(snapshot) + return snapshot + + def validate_generation( + self, + reference: GenerationReference, + ): + with self.engine.connect() as connection: + payload = connection.execute( + select(index_generations.c.payload).where( + index_generations.c.generation_id + == reference.generation_id, + ) + ).scalar_one_or_none() + if ( + payload is None + or GenerationReference.model_validate(payload, strict=False) + != reference + ): + raise IndexSchemaError( + f"Generation {reference.generation_id} is not published." + ) + return super().validate_generation(reference) + + def generation_reference( + self, + *, + generation_id: str, + media_id: str, + ) -> GenerationReference: + manifest_path = self.generation_directory(generation_id) / MANIFEST_FILE + try: + manifest = CompletedGenerationManifest.model_validate_json( + manifest_path.read_text(encoding="utf-8") + ) + reference = GenerationReference( + generation_id=generation_id, + media_id=media_id, + manifest_sha256=sha256_file(manifest_path), + input_sha256=manifest.inputs[media_id].sha256, + config_fingerprint=manifest.config_fingerprint, + modalities=tuple( + manifest.configuration["enabled_modalities"] + ), + record_counts=dict(manifest.record_counts), + store_size_bytes_at_commit=( + manifest.store_size_bytes_at_commit + ), + ) + except (OSError, KeyError, TypeError, ValueError, ValidationError) as exc: + raise IndexSchemaError( + f"Completed generation {generation_id} has an invalid manifest." + ) from exc + LocalSnapshotRepository.validate_generation(self, reference) + return reference + + def publish_generation( + self, + reference: GenerationReference, + config: IndexConfig, + ) -> IndexSnapshot: + return self._publish( + replacement=reference, + remove_media_id=None, + config_fingerprint=reference.config_fingerprint, + configuration=self.snapshot_configuration(config), + ) + + def remove(self, media_id: str) -> bool: + active = self.read_active() + if active is None or media_id not in active.generations: + return False + self._publish( + replacement=None, + remove_media_id=media_id, + config_fingerprint=active.config_fingerprint, + configuration=dict(active.configuration), + ) + return True + + def clear(self) -> bool: + active = self.read_active() + if active is None or not active.generations: + return False + self._publish( + replacement=None, + remove_media_id="*", + config_fingerprint=active.config_fingerprint, + configuration=dict(active.configuration), + ) + return True + + def _publish( + self, + *, + replacement: GenerationReference | None, + remove_media_id: str | None, + config_fingerprint: str, + configuration: dict[str, Any], + ) -> IndexSnapshot: + with self.engine.begin() as connection: + self._ensure_index_state(connection) + state = connection.execute( + select( + index_state.c.active_snapshot_id, + index_state.c.active_snapshot_sha256, + ) + .where(index_state.c.singleton_id == _INDEX_STATE_ID) + .with_for_update() + ).one() + active = ( + None + if state.active_snapshot_id is None + else self._read_snapshot( + connection, + state.active_snapshot_id, + expected_sha256=state.active_snapshot_sha256, + ) + ) + generations = dict(active.generations) if active is not None else {} + if remove_media_id == "*": + generations.clear() + elif remove_media_id is not None: + generations.pop(remove_media_id, None) + if replacement is not None: + existing = connection.execute( + select(index_generations.c.payload).where( + index_generations.c.generation_id + == replacement.generation_id + ) + ).scalar_one_or_none() + if existing is None: + connection.execute( + insert(index_generations).values( + generation_id=replacement.generation_id, + media_id=replacement.media_id, + manifest_sha256=replacement.manifest_sha256, + payload=replacement.model_dump(mode="json"), + ) + ) + elif ( + GenerationReference.model_validate(existing, strict=False) + != replacement + ): + raise IndexSchemaError( + "The generation identity is already published " + "with different metadata." + ) + other_media = set(generations) - {replacement.media_id} + if other_media and active is not None: + configuration = dict(active.configuration) + generations[replacement.media_id] = replacement + snapshot = IndexSnapshot( + snapshot_id=uuid4().hex, + created_at=datetime.now(timezone.utc), + config_fingerprint=config_fingerprint, + configuration=configuration, + generations=generations, + ) + checksum = _checksum(snapshot) + connection.execute( + insert(index_snapshots).values( + snapshot_id=snapshot.snapshot_id, + created_at=snapshot.created_at.isoformat(), + sha256=checksum, + payload=snapshot.model_dump(mode="json"), + ) + ) + connection.execute( + update(index_state) + .where(index_state.c.singleton_id == _INDEX_STATE_ID) + .values( + active_snapshot_id=snapshot.snapshot_id, + active_snapshot_sha256=checksum, + ) + ) + return snapshot + + def active_config( + self, + *, + device: str, + ) -> tuple[IndexConfig, IndexSnapshot]: + snapshot = self.read_active(required=True) + assert snapshot is not None + if not snapshot.generations: + raise IndexNotReadyError( + "The active index snapshot contains no media." + ) + return ( + self._config_for_snapshot( + snapshot, + snapshot_sha256=_checksum(snapshot), + device=device, + ), + snapshot, + ) + + def status(self) -> dict[str, Any] | None: + snapshot = self.read_active() + if snapshot is None: + return None + return snapshot_status(snapshot) diff --git a/src/vidxp/infrastructure/sql_tables.py b/src/vidxp/infrastructure/sql_tables.py new file mode 100644 index 0000000..9e1030f --- /dev/null +++ b/src/vidxp/infrastructure/sql_tables.py @@ -0,0 +1,144 @@ +from sqlalchemy import ( + BigInteger, + CheckConstraint, + Column, + ForeignKey, + Index, + JSON, + MetaData, + String, + Table, + Text, +) + + +metadata = MetaData() + +media = Table( + "media", + metadata, + Column("media_id", String(32), primary_key=True), + Column("sha256", String(64), nullable=False, unique=True), + Column("created_at", Text, nullable=False), + Column("payload", JSON, nullable=False), +) + +artifacts = Table( + "artifacts", + metadata, + Column("artifact_id", String(32), primary_key=True), + Column( + "media_id", + String(32), + ForeignKey("media.media_id"), + nullable=False, + ), + Column("created_at", Text, nullable=False), + Column("payload", JSON, nullable=False), +) +Index("artifacts_media_id", artifacts.c.media_id) + +artifact_requests = Table( + "artifact_requests", + metadata, + Column("request_key", String(64), primary_key=True), + Column( + "artifact_id", + String(32), + ForeignKey("artifacts.artifact_id"), + nullable=False, + unique=True, + ), +) + +media_import_requests = Table( + "media_import_requests", + metadata, + Column("request_key", String(64), primary_key=True), + Column("request_fingerprint", String(64), nullable=False), + Column( + "media_id", + String(32), + ForeignKey("media.media_id"), + nullable=True, + ), +) + +upload_intents = Table( + "upload_intents", + metadata, + Column("intent_id", String(32), primary_key=True), + Column("request_key", String(64), nullable=False, unique=True), + Column("original_filename", String(255), nullable=False), + Column("byte_size", BigInteger, nullable=False), + Column("declared_mime_type", String(127), nullable=True), + Column("state", String(32), nullable=False), + Column("created_at", Text, nullable=False), + Column("expires_at", Text, nullable=False), + Column("upload_id", String(255), nullable=True, unique=True), + Column("job_id", String(36), nullable=True, unique=True), + Column( + "media_id", + String(32), + ForeignKey("media.media_id"), + nullable=True, + ), +) + +upload_quota = Table( + "upload_quota", + metadata, + Column("singleton_id", String(1), primary_key=True), + Column("reserved_bytes", BigInteger, nullable=False, default=0), + CheckConstraint( + "reserved_bytes >= 0", + name="upload_quota_reserved_bytes_nonnegative", + ), + CheckConstraint( + "singleton_id = '1'", + name="upload_quota_singleton", + ), +) + +index_state = Table( + "index_state", + metadata, + Column("singleton_id", String(1), primary_key=True), + Column("active_snapshot_id", String(32), nullable=True), + Column("active_snapshot_sha256", String(64), nullable=True), + CheckConstraint( + "singleton_id = '1'", + name="index_state_singleton", + ), +) + +index_generations = Table( + "index_generations", + metadata, + Column("generation_id", String(32), primary_key=True), + Column("media_id", String(32), nullable=False), + Column("manifest_sha256", String(64), nullable=False), + Column("payload", JSON, nullable=False), +) +Index( + "index_generations_media", + index_generations.c.media_id, +) + +index_snapshots = Table( + "index_snapshots", + metadata, + Column("snapshot_id", String(32), primary_key=True), + Column("created_at", Text, nullable=False), + Column("sha256", String(64), nullable=False), + Column("payload", JSON, nullable=False), +) +Index( + "index_snapshots_created", + index_snapshots.c.created_at, +) +Index( + "upload_intents_expiry", + upload_intents.c.expires_at, + upload_intents.c.state, +) diff --git a/src/vidxp/infrastructure/tusd_contracts.py b/src/vidxp/infrastructure/tusd_contracts.py new file mode 100644 index 0000000..12e51c6 --- /dev/null +++ b/src/vidxp/infrastructure/tusd_contracts.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class TusdModel(BaseModel): + model_config = ConfigDict( + extra="ignore", + frozen=True, + populate_by_name=True, + ) + + +class TusdHTTPRequest(TusdModel): + method: str = Field(default="", alias="Method") + uri: str = Field(default="", alias="URI") + headers: dict[str, list[str]] = Field( + default_factory=dict, + alias="Header", + ) + + def header(self, name: str) -> tuple[str, ...]: + lowered = name.lower() + for key, values in self.headers.items(): + if key.lower() == lowered: + return tuple(values) + return () + + +class TusdUpload(TusdModel): + upload_id: str = Field(default="", alias="ID") + size: int = Field(alias="Size", ge=0) + offset: int = Field(default=0, alias="Offset", ge=0) + metadata: dict[str, str] = Field( + default_factory=dict, + alias="MetaData", + ) + size_is_deferred: bool = Field( + default=False, + alias="SizeIsDeferred", + ) + is_partial: bool = Field(default=False, alias="IsPartial") + is_final: bool = Field(default=False, alias="IsFinal") + partial_uploads: list[str] | None = Field( + default=None, + alias="PartialUploads", + ) + + +class TusdEvent(TusdModel): + upload: TusdUpload = Field(alias="Upload") + request: TusdHTTPRequest = Field(alias="HTTPRequest") + + +class TusdHookRequest(TusdModel): + hook_type: Literal[ + "pre-create", + "post-finish", + "pre-terminate", + "post-terminate", + ] = Field(alias="Type") + event: TusdEvent = Field(alias="Event") + + +class TusdHTTPResponse(TusdModel): + status_code: int = Field(alias="StatusCode", ge=100, le=599) + body: str = Field(default="", alias="Body") + headers: dict[str, str] = Field(default_factory=dict, alias="Header") + + +class TusdChangeFileInfo(TusdModel): + upload_id: str = Field(alias="ID") + + +class TusdHookResponse(TusdModel): + reject_upload: bool = Field(default=False, alias="RejectUpload") + reject_termination: bool = Field( + default=False, + alias="RejectTermination", + ) + http_response: TusdHTTPResponse | None = Field( + default=None, + alias="HTTPResponse", + ) + change_file_info: TusdChangeFileInfo | None = Field( + default=None, + alias="ChangeFileInfo", + ) + + def wire(self) -> dict[str, Any]: + return self.model_dump( + mode="json", + by_alias=True, + exclude_none=True, + ) diff --git a/src/vidxp/job_service.py b/src/vidxp/job_service.py new file mode 100644 index 0000000..dcad2df --- /dev/null +++ b/src/vidxp/job_service.py @@ -0,0 +1,340 @@ +from __future__ import annotations + +from functools import wraps +from time import sleep +from typing import Any, Callable, Protocol + +from vidxp.application_models import ( + ActorOverlayJobRequest, + ApplicationError, + ComponentReadiness, + CreateActorOverlayCommand, + CreateIndexCommand, + CreateSnippetCommand, + ErrorCategory, + IndexJobRequest, + Job, + JobKind, + JobPage, + JobQueue, + JobResult, + JobState, + InvalidRequestError, + ListJobsCommand, + MediaImportJobRequest, + PrepareModelsCommand, + PrepareModelsJobRequest, + QueryJobRequest, + QueryVideoCommand, + ResourceNotFoundError, + SearchCommand, + SearchJobRequest, + SnippetJobRequest, +) +from vidxp.ports import ( + InvalidJobBackendRequestError, + JobBackend, + JobIdempotencyConflictError, +) +from vidxp.settings import VidXPSettings + + +class ReadJobPlanner(Protocol): + """Resolve immutable index identities before durable read jobs enqueue.""" + + def plan_search(self, command: SearchCommand) -> SearchJobRequest: ... + + def plan_query(self, command: QueryVideoCommand) -> QueryJobRequest: ... + + def plan_actor_overlay( + self, + command: CreateActorOverlayCommand, + ) -> ActorOverlayJobRequest: ... + + +def job_boundary(handler: Callable) -> Callable: + """Translate job-backend failures once for every adapter.""" + + @wraps(handler) + def wrapped(*args: Any, **kwargs: Any) -> Any: + try: + return handler(*args, **kwargs) + except ApplicationError: + raise + except InvalidJobBackendRequestError as exc: + raise InvalidRequestError() from exc + except JobIdempotencyConflictError as exc: + raise ApplicationError( + "idempotency_key_reused", + ErrorCategory.validation, + "The idempotency key was already used for another request.", + ) from exc + except Exception as exc: + raise ApplicationError( + "job_backend_unavailable", + ErrorCategory.unavailable, + "The durable job backend is unavailable.", + retryable=True, + ) from exc + + return wrapped + + +class JobService: + """Transport-neutral durable job commands backed by one workflow engine.""" + + def __init__( + self, + *, + settings: VidXPSettings, + backend: JobBackend, + read_planner: ReadJobPlanner | None = None, + ) -> None: + self.settings = settings + self.backend = backend + self.read_planner = read_planner + + def _read_job_planner(self) -> ReadJobPlanner: + if self.read_planner is None: + raise ApplicationError( + "read_job_planner_unavailable", + ErrorCategory.unavailable, + "Durable index reads are not configured.", + ) + return self.read_planner + + @job_boundary + def start(self) -> None: + self.backend.start() + + @job_boundary + def submit_index( + self, + command: CreateIndexCommand, + *, + job_id: str | None = None, + ) -> Job: + return self.backend.submit( + IndexJobRequest(command=command), + queue=self._model_queue(), + job_id=job_id, + ) + + @job_boundary + def submit_search( + self, + command: SearchCommand, + *, + job_id: str | None = None, + ) -> Job: + return self.backend.submit( + self._read_job_planner().plan_search(command), + queue=self._model_queue(), + job_id=job_id, + ) + + @job_boundary + def submit_query( + self, + command: QueryVideoCommand, + *, + job_id: str | None = None, + ) -> Job: + return self.backend.submit( + self._read_job_planner().plan_query(command), + queue=self._model_queue(), + job_id=job_id, + ) + + @job_boundary + def submit_snippet( + self, + command: CreateSnippetCommand, + *, + job_id: str | None = None, + ) -> Job: + return self.backend.submit( + SnippetJobRequest(command=command), + queue=JobQueue.cpu, + job_id=job_id, + ) + + @job_boundary + def submit_actor_overlay( + self, + command: CreateActorOverlayCommand, + *, + job_id: str | None = None, + ) -> Job: + return self.backend.submit( + self._read_job_planner().plan_actor_overlay(command), + queue=JobQueue.cpu, + job_id=job_id, + ) + + @job_boundary + def submit_prepare_models( + self, + command: PrepareModelsCommand, + *, + job_id: str | None = None, + ) -> Job: + return self.backend.submit( + PrepareModelsJobRequest(command=command), + queue=self._model_queue(), + job_id=job_id, + ) + + def enqueue_media_import_in_transaction( + self, + upload_id: str, + *, + connection: Any, + job_id: str, + ) -> str: + enqueue = getattr(self.backend, "enqueue_in_transaction", None) + if enqueue is None: + raise RuntimeError( + "The durable job backend cannot join an upload transaction." + ) + return enqueue( + connection, + MediaImportJobRequest(upload_id=upload_id), + queue=JobQueue.cpu, + job_id=job_id, + ) + + @job_boundary + def get(self, job_id: str) -> Job: + job = self.backend.get(job_id) + if job is None: + raise ResourceNotFoundError("job") + return job + + @job_boundary + def list(self, command: ListJobsCommand) -> JobPage: + return self.backend.list(command) + + @job_boundary + def cancel(self, job_id: str) -> Job: + current = self.get(job_id) + if current.kind == JobKind.media_import: + raise ApplicationError( + "job_not_cancellable", + ErrorCategory.conflict, + "Media import jobs are managed by the upload lifecycle.", + ) + if current.state in { + JobState.succeeded, + JobState.failed, + JobState.cancelled, + JobState.recovery_exhausted, + }: + return current + cancelled = self.backend.cancel(job_id) + if cancelled is None: + raise ResourceNotFoundError("job") + return cancelled + + @job_boundary + def retry( + self, + job_id: str, + *, + retry_id: str | None = None, + ) -> Job: + current = self.get(job_id) + if current.kind == JobKind.media_import: + raise ApplicationError( + "job_not_retryable", + ErrorCategory.conflict, + "Create a new upload intent to retry a media import.", + ) + if current.state not in { + JobState.failed, + JobState.cancelled, + JobState.recovery_exhausted, + }: + raise ApplicationError( + "job_not_retryable", + ErrorCategory.conflict, + "Only failed or cancelled jobs can be retried.", + ) + retried = self.backend.retry(job_id, retry_id=retry_id) + if retried is None: + raise ResourceNotFoundError("job") + return retried + + @job_boundary + def result(self, job_id: str) -> JobResult: + job = self.get(job_id) + if job.state == JobState.cancelled: + raise ApplicationError( + "job_cancelled", + ErrorCategory.cancelled, + "The job was cancelled.", + ) + if job.state != JobState.succeeded: + if job.error is not None: + raise ApplicationError( + job.error.code, + job.error.category, + job.error.message, + details=job.error.details, + retryable=job.error.retryable, + ) + raise ApplicationError( + "job_not_complete", + ErrorCategory.conflict, + "The job has not completed successfully.", + retryable=job.state in {JobState.queued, JobState.running}, + ) + if job.result is None: + raise ApplicationError( + "job_result_unavailable", + ErrorCategory.internal, + "The completed job result is unavailable.", + ) + return job.result + + @job_boundary + def wait( + self, + job_id: str, + *, + progress: Callable[[Job], None] | None = None, + ) -> Job: + last_progress = None + while True: + job = self.get(job_id) + if progress is not None and job.progress != last_progress: + progress(job) + last_progress = job.progress + if job.state not in {JobState.queued, JobState.running}: + if job.state != JobState.succeeded: + self.result(job.job_id) + return job + sleep(self.settings.workflow_poll_interval_seconds) + + def _model_queue(self) -> JobQueue: + return ( + JobQueue.gpu + if self.settings.runtime_backend.startswith("cuda") + else JobQueue.cpu + ) + + @job_boundary + def readiness(self) -> ComponentReadiness: + self.backend.health() + return ComponentReadiness( + name="workflow", + ready=True, + message="The durable workflow database is available.", + ) + + @job_boundary + def stop_worker(self) -> bool: + return self.backend.stop_worker() + + def close(self) -> None: + self.backend.close() diff --git a/src/vidxp/media_runtime.py b/src/vidxp/media_runtime.py new file mode 100644 index 0000000..0fad17a --- /dev/null +++ b/src/vidxp/media_runtime.py @@ -0,0 +1,317 @@ +from __future__ import annotations + +import os +import shlex +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Literal +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict, Field + +from vidxp.app_paths import default_config_directory + + +MEDIA_RUNTIME_SCHEMA_VERSION = 1 +MEDIA_RUNTIME_FILENAME = "media-runtime.json" +REQUIRED_FFMPEG_ENCODERS = ("libx264", "aac") + + +class MediaRuntimeModel(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + +class MediaRuntimeConfiguration(MediaRuntimeModel): + schema_version: Literal[MEDIA_RUNTIME_SCHEMA_VERSION] = ( + MEDIA_RUNTIME_SCHEMA_VERSION + ) + ffmpeg_executable: Path + ffprobe_executable: Path + + +class SystemInstallPlan(MediaRuntimeModel): + manager: str = Field(min_length=1) + command: tuple[str, ...] = Field(min_length=1) + automatic: bool + + @property + def display_command(self) -> str: + if sys.platform == "win32": + return subprocess.list2cmdline(self.command) + return shlex.join(self.command) + + +class MediaRuntimeStatus(MediaRuntimeModel): + ready: bool + initialized: bool + ffmpeg_executable: Path | None = None + ffprobe_executable: Path | None = None + required_encoders: tuple[str, ...] = REQUIRED_FFMPEG_ENCODERS + errors: tuple[str, ...] = () + install_plan: SystemInstallPlan | None = None + + +def media_runtime_config_path( + config_directory: Path | None = None, +) -> Path: + return (config_directory or default_config_directory()) / ( + MEDIA_RUNTIME_FILENAME + ) + + +def load_media_runtime_configuration( + config_directory: Path | None = None, +) -> MediaRuntimeConfiguration | None: + path = media_runtime_config_path(config_directory) + try: + contents = path.read_text(encoding="utf-8") + except FileNotFoundError: + return None + return MediaRuntimeConfiguration.model_validate_json(contents) + + +def default_media_executable(name: Literal["ffmpeg", "ffprobe"]) -> str: + try: + configuration = load_media_runtime_configuration() + except (OSError, ValueError): + return name + if configuration is None: + return name + return str(getattr(configuration, f"{name}_executable")) + + +def _resolve_executable(value: str | Path) -> Path | None: + candidate = Path(value).expanduser() + if candidate.is_absolute() or candidate.parent != Path("."): + return candidate.resolve() if candidate.is_file() else None + resolved = shutil.which(str(value)) + return Path(resolved).resolve() if resolved else None + + +def _command_output(arguments: list[str], *, timeout: float = 15) -> str: + completed = subprocess.run( + arguments, + check=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=timeout, + ) + return completed.stdout.decode(errors="replace") + + +def _verify_ffmpeg(path: Path) -> tuple[str, ...]: + errors = [] + try: + _command_output([str(path), "-version"]) + encoders = _command_output([str(path), "-hide_banner", "-encoders"]) + except (OSError, subprocess.SubprocessError) as exc: + return (f"FFmpeg could not be executed: {exc}",) + available = { + token + for line in encoders.splitlines() + for token in line.split() + } + missing = tuple( + encoder + for encoder in REQUIRED_FFMPEG_ENCODERS + if encoder not in available + ) + if missing: + errors.append( + "FFmpeg is missing required encoder(s): " + ", ".join(missing) + ) + return tuple(errors) + + +def _verify_ffprobe(path: Path) -> tuple[str, ...]: + try: + _command_output([str(path), "-version"]) + except (OSError, subprocess.SubprocessError) as exc: + return (f"ffprobe could not be executed: {exc}",) + return () + + +def system_install_plan() -> SystemInstallPlan | None: + if sys.platform == "win32": + if shutil.which("winget"): + return SystemInstallPlan( + manager="Windows Package Manager", + command=( + "winget", + "install", + "--id", + "Gyan.FFmpeg", + "--exact", + "--source", + "winget", + "--accept-package-agreements", + "--accept-source-agreements", + ), + automatic=True, + ) + return None + if sys.platform == "darwin": + brew = shutil.which("brew") + if brew is None: + for candidate in ( + Path("/opt/homebrew/bin/brew"), + Path("/usr/local/bin/brew"), + ): + if candidate.is_file(): + brew = str(candidate) + break + if brew: + return SystemInstallPlan( + manager="Homebrew", + command=(brew, "install", "ffmpeg"), + automatic=True, + ) + return None + if shutil.which("apt-get"): + return SystemInstallPlan( + manager="APT", + command=("sudo", "apt-get", "install", "ffmpeg"), + automatic=False, + ) + if shutil.which("dnf"): + return SystemInstallPlan( + manager="DNF", + command=("sudo", "dnf", "install", "ffmpeg"), + automatic=False, + ) + if shutil.which("pacman"): + return SystemInstallPlan( + manager="pacman", + command=("sudo", "pacman", "-S", "ffmpeg"), + automatic=False, + ) + return None + + +def inspect_media_runtime( + *, + ffmpeg: str | Path | None = None, + ffprobe: str | Path | None = None, + config_directory: Path | None = None, +) -> MediaRuntimeStatus: + configuration = None + configuration_error = None + try: + configuration = load_media_runtime_configuration(config_directory) + except (OSError, ValueError) as exc: + configuration_error = f"The saved media runtime is invalid: {exc}" + + ffmpeg_value = ( + ffmpeg + or ( + configuration.ffmpeg_executable + if configuration is not None + else "ffmpeg" + ) + ) + ffprobe_value = ( + ffprobe + or ( + configuration.ffprobe_executable + if configuration is not None + else "ffprobe" + ) + ) + ffmpeg_path = _resolve_executable(ffmpeg_value) + ffprobe_path = _resolve_executable(ffprobe_value) + errors = [] + if configuration_error: + errors.append(configuration_error) + if ffmpeg_path is None: + errors.append(f"FFmpeg was not found: {ffmpeg_value}") + else: + errors.extend(_verify_ffmpeg(ffmpeg_path)) + if ffprobe_path is None: + errors.append(f"ffprobe was not found: {ffprobe_value}") + else: + errors.extend(_verify_ffprobe(ffprobe_path)) + return MediaRuntimeStatus( + ready=not errors, + initialized=configuration is not None, + ffmpeg_executable=ffmpeg_path, + ffprobe_executable=ffprobe_path, + errors=tuple(errors), + install_plan=system_install_plan() if errors else None, + ) + + +def save_media_runtime_configuration( + status: MediaRuntimeStatus, + *, + config_directory: Path | None = None, +) -> MediaRuntimeConfiguration: + if ( + not status.ready + or status.ffmpeg_executable is None + or status.ffprobe_executable is None + ): + raise ValueError("Only a verified media runtime can be saved.") + configuration = MediaRuntimeConfiguration( + ffmpeg_executable=status.ffmpeg_executable, + ffprobe_executable=status.ffprobe_executable, + ) + destination = media_runtime_config_path(config_directory) + destination.parent.mkdir(parents=True, exist_ok=True) + temporary = destination.with_name( + f".{destination.name}.{uuid4().hex}.tmp" + ) + try: + temporary.write_text( + configuration.model_dump_json(indent=2), + encoding="utf-8", + ) + temporary.replace(destination) + finally: + temporary.unlink(missing_ok=True) + return configuration + + +def install_media_runtime( + plan: SystemInstallPlan, + *, + output_to_stderr: bool = False, +) -> None: + if not plan.automatic: + raise ValueError( + "This package-manager command must be run in a system terminal." + ) + destination = sys.stderr if output_to_stderr else None + subprocess.run( + list(plan.command), + check=True, + stdout=destination, + stderr=destination, + ) + + +def explicitly_configured_by_environment() -> bool: + return bool( + os.environ.get("VIDXP_FFMPEG_EXECUTABLE") + and os.environ.get("VIDXP_FFPROBE_EXECUTABLE") + ) + + +def media_runtime_is_initialized( + config_directory: Path | None = None, +) -> bool: + if explicitly_configured_by_environment(): + return True + try: + configuration = load_media_runtime_configuration(config_directory) + except (OSError, ValueError): + return False + if bool( + configuration is not None + and configuration.ffmpeg_executable.is_file() + and configuration.ffprobe_executable.is_file() + ): + return True + return bool(shutil.which("ffmpeg") and shutil.which("ffprobe")) diff --git a/src/vidxp/media_service.py b/src/vidxp/media_service.py new file mode 100644 index 0000000..97dd12a --- /dev/null +++ b/src/vidxp/media_service.py @@ -0,0 +1,313 @@ +from __future__ import annotations + +import json +from pathlib import Path +import hashlib +from uuid import uuid4 + +from vidxp.application_models import ( + ImportMediaCommand, + ListMediaCommand, + MediaAsset, + MediaPage, +) +from vidxp.core.media import ( + MediaRecord, + MediaState, + QuarantinedMedia, + StagedMedia, + MediaUnavailableError, + utc_now, +) +from vidxp.core.cursors import ( + CursorError, + decode_offset_cursor, + encode_offset_cursor, +) +from vidxp.ports import ( + LocalFileResource, + MediaCatalogPort, + MediaProbePort, + MediaStorePort, +) +from vidxp.settings import ApplicationMode, VidXPSettings + + +class MediaImportNotAllowedError(PermissionError): + """Raised when a local path is outside the local import policy.""" + + +class MediaIdempotencyConflictError(FileExistsError): + """Raised when an import key is reused for different media content.""" + + +def media_asset(record: MediaRecord) -> MediaAsset: + return MediaAsset( + schema_version=record.schema_version, + media_id=record.media_id, + video_id=record.video_id, + original_filename=record.original_filename, + sha256=record.sha256, + byte_size=record.byte_size, + declared_mime_type=record.declared_mime_type, + detected_mime_type=record.detected_mime_type, + container=record.container, + duration_seconds=record.duration_seconds, + streams=record.streams, + state=record.state, + created_at=record.created_at, + ) + + +class MediaService: + def __init__( + self, + *, + settings: VidXPSettings, + catalog: MediaCatalogPort, + store: MediaStorePort, + probe: MediaProbePort, + ) -> None: + self.settings = settings + self.catalog = catalog + self.store = store + self.probe = probe + + def import_local(self, command: ImportMediaCommand) -> MediaAsset: + if self.settings.mode != ApplicationMode.local: + raise MediaImportNotAllowedError( + "Local path imports are available only in local mode." + ) + source = self._resolve_import_source(command.path) + return self._import( + source, + original_filename=command.original_filename or source.name, + declared_mime_type=command.declared_mime_type, + ) + + def import_quarantined( + self, + media: QuarantinedMedia, + *, + request_key: str | None = None, + ) -> MediaAsset: + try: + source = media.path.resolve(strict=True) + except (FileNotFoundError, OSError) as exc: + raise MediaUnavailableError( + "The quarantined media source is unavailable." + ) from exc + if not source.is_file(): + raise MediaUnavailableError( + "The quarantined media source is not a file." + ) + return self._import( + source, + original_filename=media.original_filename, + declared_mime_type=media.declared_mime_type, + request_key=request_key, + ) + + def _import( + self, + source: Path, + *, + original_filename: str, + declared_mime_type: str | None, + request_key: str | None = None, + ) -> MediaAsset: + staged = self.store.stage_local(source) + try: + with self.store.publication_lock(staged.sha256): + if request_key is not None: + request_fingerprint = self._import_fingerprint( + staged, + original_filename=original_filename, + declared_mime_type=declared_mime_type, + ) + try: + completed = self.catalog.reserve_media_import( + request_key, + request_fingerprint, + ) + except FileExistsError as exc: + raise MediaIdempotencyConflictError from exc + if completed is not None: + return media_asset(completed) + result = self._publish_import( + original_filename=original_filename, + declared_mime_type=declared_mime_type, + staged=staged, + ) + if request_key is not None: + record = self.catalog.get_media(result.media_id) + if record is None: + raise RuntimeError( + "The imported media record is unavailable." + ) + try: + self.catalog.complete_media_import( + request_key, + request_fingerprint, + record, + ) + except FileExistsError as exc: + raise MediaIdempotencyConflictError from exc + return result + finally: + self.store.discard(staged) + + @staticmethod + def _import_fingerprint( + staged: StagedMedia, + *, + original_filename: str, + declared_mime_type: str | None, + ) -> str: + payload = json.dumps( + { + "version": 1, + "sha256": staged.sha256, + "original_filename": original_filename, + "declared_mime_type": declared_mime_type, + }, + sort_keys=True, + separators=(",", ":"), + ).encode() + return hashlib.sha256(payload).hexdigest() + + def _publish_import( + self, + *, + original_filename: str, + declared_mime_type: str | None, + staged: StagedMedia, + ) -> MediaAsset: + if existing := self.catalog.get_media_by_checksum(staged.sha256): + self.store.publish( + staged.model_copy( + update={"storage_key": existing.storage_key} + ) + ) + return media_asset(existing) + probe = self.probe.probe(staged.path) + stored = self.store.publish(staged) + media_id = uuid4().hex + record = MediaRecord( + media_id=media_id, + video_id=media_id, + sha256=stored.sha256, + original_filename=original_filename, + byte_size=stored.byte_size, + declared_mime_type=declared_mime_type, + detected_mime_type=probe.detected_mime_type, + container=probe.container, + duration_seconds=probe.duration_seconds, + streams=probe.streams, + storage_key=stored.storage_key, + state=MediaState.ready, + created_at=utc_now(), + ) + try: + authoritative = self.catalog.put_media(record) + except BaseException: + try: + retained = self.catalog.get_media_by_checksum(stored.sha256) + except Exception: + retained = None + if retained is None: + try: + self.store.delete(stored.storage_key) + except OSError: + pass + raise + if authoritative.storage_key != stored.storage_key: + try: + self.store.delete(stored.storage_key) + except OSError: + pass + return media_asset(authoritative) + + def get(self, media_id: str) -> MediaAsset: + return media_asset(self.require_record(media_id)) + + def list(self, command: ListMediaCommand) -> MediaPage: + scope = hashlib.sha256( + str(self.settings.repository_root.resolve()).encode() + ).hexdigest() + try: + offset = decode_offset_cursor(command.cursor, scope=scope) + except CursorError as exc: + raise ValueError("The media cursor is invalid.") from exc + total = self.catalog.count_media() + if offset > total: + raise ValueError("The media cursor is outside the result set.") + items = tuple( + media_asset(record) + for record in self.catalog.list_media( + limit=command.page_size, + offset=offset, + ) + ) + next_offset = offset + len(items) + cursor = ( + encode_offset_cursor(next_offset, scope=scope) + if next_offset < total + else None + ) + return MediaPage( + items=items, + total=total, + next_cursor=cursor, + ) + + def require_record(self, media_id: str) -> MediaRecord: + record = self.catalog.get_media(media_id) + if record is None or record.state != MediaState.ready: + raise MediaUnavailableError("The media asset is unavailable.") + return record + + def content(self, media_id: str) -> LocalFileResource: + record = self.require_record(media_id) + try: + path = self.store.verify( + record.storage_key, + sha256=record.sha256, + byte_size=record.byte_size, + ) + except (FileNotFoundError, PermissionError) as exc: + raise MediaUnavailableError( + "The managed media content is unavailable." + ) from exc + return LocalFileResource( + path=path, + filename=record.original_filename, + mime_type=record.detected_mime_type, + byte_size=record.byte_size, + etag=record.sha256, + ) + + def _resolve_import_source(self, path: Path) -> Path: + try: + source = path.expanduser().resolve(strict=True) + except (FileNotFoundError, OSError) as exc: + raise MediaUnavailableError( + "The local media source is unavailable." + ) from exc + if not source.is_file(): + raise MediaUnavailableError( + "The local media source is not a file." + ) + roots = self.settings.trusted_local_import_roots + if not roots: + return source + for configured in roots: + try: + root = configured.expanduser().resolve(strict=True) + except (FileNotFoundError, OSError): + continue + if root.is_dir() and source.is_relative_to(root): + return source + raise MediaImportNotAllowedError( + "The local media source is outside the trusted import roots." + ) diff --git a/src/vidxp/migrations/__init__.py b/src/vidxp/migrations/__init__.py new file mode 100644 index 0000000..8038de5 --- /dev/null +++ b/src/vidxp/migrations/__init__.py @@ -0,0 +1 @@ +"""VidXP Alembic migration package.""" diff --git a/src/vidxp/migrations/env.py b/src/vidxp/migrations/env.py new file mode 100644 index 0000000..106d917 --- /dev/null +++ b/src/vidxp/migrations/env.py @@ -0,0 +1,39 @@ +from alembic import context +from sqlalchemy import engine_from_config, pool + +from vidxp.infrastructure.sql_tables import metadata + + +def run_migrations_offline() -> None: + context.configure( + url=context.config.get_main_option("sqlalchemy.url"), + target_metadata=metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + engine = engine_from_config( + context.config.get_section(context.config.config_ini_section) or {}, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with engine.connect() as connection: + context.configure( + connection=connection, + target_metadata=metadata, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + engine.dispose() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/src/vidxp/migrations/versions/20260729_01_catalog_uploads.py b/src/vidxp/migrations/versions/20260729_01_catalog_uploads.py new file mode 100644 index 0000000..a94f834 --- /dev/null +++ b/src/vidxp/migrations/versions/20260729_01_catalog_uploads.py @@ -0,0 +1,173 @@ +"""Create the single-repository server catalog. + +Revision ID: 20260729_01 +Revises: +Create Date: 2026-07-29 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = "20260729_01" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "media", + sa.Column("media_id", sa.String(length=32), nullable=False), + sa.Column("sha256", sa.String(length=64), nullable=False), + sa.Column("created_at", sa.Text(), nullable=False), + sa.Column("payload", sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint("media_id"), + sa.UniqueConstraint("sha256"), + ) + op.create_table( + "artifacts", + sa.Column("artifact_id", sa.String(length=32), nullable=False), + sa.Column("media_id", sa.String(length=32), nullable=False), + sa.Column("created_at", sa.Text(), nullable=False), + sa.Column("payload", sa.JSON(), nullable=False), + sa.ForeignKeyConstraint(["media_id"], ["media.media_id"]), + sa.PrimaryKeyConstraint("artifact_id"), + ) + op.create_index( + "artifacts_media_id", + "artifacts", + ["media_id"], + unique=False, + ) + op.create_table( + "artifact_requests", + sa.Column("request_key", sa.String(length=64), nullable=False), + sa.Column("artifact_id", sa.String(length=32), nullable=False), + sa.ForeignKeyConstraint( + ["artifact_id"], + ["artifacts.artifact_id"], + ), + sa.PrimaryKeyConstraint("request_key"), + sa.UniqueConstraint("artifact_id"), + ) + op.create_table( + "media_import_requests", + sa.Column("request_key", sa.String(length=64), nullable=False), + sa.Column( + "request_fingerprint", + sa.String(length=64), + nullable=False, + ), + sa.Column("media_id", sa.String(length=32), nullable=True), + sa.ForeignKeyConstraint(["media_id"], ["media.media_id"]), + sa.PrimaryKeyConstraint("request_key"), + ) + op.create_table( + "index_state", + sa.Column("singleton_id", sa.String(length=1), nullable=False), + sa.Column("active_snapshot_id", sa.String(length=32), nullable=True), + sa.Column( + "active_snapshot_sha256", + sa.String(length=64), + nullable=True, + ), + sa.CheckConstraint( + "singleton_id = '1'", + name="index_state_singleton", + ), + sa.PrimaryKeyConstraint("singleton_id"), + ) + op.create_table( + "index_generations", + sa.Column("generation_id", sa.String(length=32), nullable=False), + sa.Column("media_id", sa.String(length=32), nullable=False), + sa.Column("manifest_sha256", sa.String(length=64), nullable=False), + sa.Column("payload", sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint("generation_id"), + ) + op.create_index( + "index_generations_media", + "index_generations", + ["media_id"], + unique=False, + ) + op.create_table( + "index_snapshots", + sa.Column("snapshot_id", sa.String(length=32), nullable=False), + sa.Column("created_at", sa.Text(), nullable=False), + sa.Column("sha256", sa.String(length=64), nullable=False), + sa.Column("payload", sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint("snapshot_id"), + ) + op.create_index( + "index_snapshots_created", + "index_snapshots", + ["created_at"], + unique=False, + ) + op.create_table( + "upload_quota", + sa.Column("singleton_id", sa.String(length=1), nullable=False), + sa.Column("reserved_bytes", sa.BigInteger(), nullable=False), + sa.CheckConstraint( + "reserved_bytes >= 0", + name="upload_quota_reserved_bytes_nonnegative", + ), + sa.CheckConstraint( + "singleton_id = '1'", + name="upload_quota_singleton", + ), + sa.PrimaryKeyConstraint("singleton_id"), + ) + op.create_table( + "upload_intents", + sa.Column("intent_id", sa.String(length=32), nullable=False), + sa.Column("request_key", sa.String(length=64), nullable=False), + sa.Column("original_filename", sa.String(length=255), nullable=False), + sa.Column("byte_size", sa.BigInteger(), nullable=False), + sa.Column( + "declared_mime_type", + sa.String(length=127), + nullable=True, + ), + sa.Column("state", sa.String(length=32), nullable=False), + sa.Column("created_at", sa.Text(), nullable=False), + sa.Column("expires_at", sa.Text(), nullable=False), + sa.Column("upload_id", sa.String(length=255), nullable=True), + sa.Column("job_id", sa.String(length=36), nullable=True), + sa.Column("media_id", sa.String(length=32), nullable=True), + sa.ForeignKeyConstraint(["media_id"], ["media.media_id"]), + sa.PrimaryKeyConstraint("intent_id"), + sa.UniqueConstraint("job_id"), + sa.UniqueConstraint("request_key"), + sa.UniqueConstraint("upload_id"), + ) + op.create_index( + "upload_intents_expiry", + "upload_intents", + ["expires_at", "state"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index("upload_intents_expiry", table_name="upload_intents") + op.drop_table("upload_intents") + op.drop_table("upload_quota") + op.drop_index( + "index_snapshots_created", + table_name="index_snapshots", + ) + op.drop_table("index_snapshots") + op.drop_index( + "index_generations_media", + table_name="index_generations", + ) + op.drop_table("index_generations") + op.drop_table("index_state") + op.drop_table("media_import_requests") + op.drop_table("artifact_requests") + op.drop_index("artifacts_media_id", table_name="artifacts") + op.drop_table("artifacts") + op.drop_table("media") diff --git a/src/vidxp/migrations/versions/__init__.py b/src/vidxp/migrations/versions/__init__.py new file mode 100644 index 0000000..8d639b7 --- /dev/null +++ b/src/vidxp/migrations/versions/__init__.py @@ -0,0 +1 @@ +"""Reviewed VidXP database revisions.""" diff --git a/src/vidxp/model_contracts.py b/src/vidxp/model_contracts.py new file mode 100644 index 0000000..6b6788e --- /dev/null +++ b/src/vidxp/model_contracts.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +import re +from typing import Any + + +_IMMUTABLE_REVISION = re.compile(r"^[0-9a-f]{40,64}$") +_SHA256 = re.compile(r"^[0-9a-f]{64}$") + + +def loaded_compute_precision(model: Any, *, fallback: str) -> str: + try: + value = next(model.parameters()).dtype + except (AttributeError, StopIteration, TypeError): + return fallback + return str(value).removeprefix("torch.") + + +class ModelArtifactUnavailableError(RuntimeError): + def __init__(self, capability: str) -> None: + self.capability = capability + super().__init__(f"Model artifacts for {capability} are unavailable.") + + +@dataclass(frozen=True) +class ModelKey: + capability: str + provider: str + model_id: str + revision: str + device: str + + +@dataclass(frozen=True) +class ModelSpec: + capability: str + provider: str + model_id: str + revision: str + download_size_bytes: int + weights_file: str + weights_sha256: str + license: str + weights_precision: str + + def __post_init__(self) -> None: + if not _IMMUTABLE_REVISION.fullmatch(self.revision): + raise ValueError("Model revisions must be immutable commit hashes.") + if self.download_size_bytes <= 0: + raise ValueError("Model download sizes must be positive.") + if not _SHA256.fullmatch(self.weights_sha256): + raise ValueError("Model weight checksums must be SHA-256 hashes.") + + def key(self, device: str) -> ModelKey: + return ModelKey( + capability=self.capability, + provider=self.provider, + model_id=self.model_id, + revision=self.revision, + device=device, + ) + + def identity(self, *, cached: bool | None = None) -> dict[str, Any]: + identity = { + "provider": self.provider, + "model": self.model_id, + "revision": self.revision, + "download_size_bytes": self.download_size_bytes, + "weights": { + "file": self.weights_file, + "sha256": self.weights_sha256, + }, + "license": self.license, + "weights_precision": self.weights_precision, + } + if cached is not None: + identity["cached"] = cached + return identity + + +@dataclass(frozen=True) +class ArtifactSpec: + capability: str + provider: str + model_id: str + revision: str + download_size_bytes: int + url: str + filename: str + sha256: str + license: str + weights_precision: str + + def __post_init__(self) -> None: + if not _IMMUTABLE_REVISION.fullmatch(self.revision): + raise ValueError("Artifact revisions must be immutable commit hashes.") + if self.download_size_bytes <= 0: + raise ValueError("Artifact download sizes must be positive.") + if not _SHA256.fullmatch(self.sha256): + raise ValueError("Artifact checksums must be SHA-256 hashes.") + if not self.url.startswith("https://"): + raise ValueError("Artifact URLs must use HTTPS.") + + def key(self, device: str) -> ModelKey: + return ModelKey( + capability=self.capability, + provider=self.provider, + model_id=self.model_id, + revision=self.revision, + device=device, + ) + + def identity(self, *, cached: bool | None = None) -> dict[str, Any]: + identity = { + "provider": self.provider, + "model": self.model_id, + "revision": self.revision, + "download_size_bytes": self.download_size_bytes, + "artifact": { + "file": self.filename, + "sha256": self.sha256, + "url": self.url, + }, + "license": self.license, + "weights_precision": self.weights_precision, + } + if cached is not None: + identity["cached"] = cached + return identity + + +def model_artifact_path( + cache: Path, + spec: ModelSpec | ArtifactSpec, +) -> Path: + if isinstance(spec, ModelSpec): + repository = "models--" + spec.model_id.replace("/", "--") + return ( + cache + / repository + / "snapshots" + / spec.revision + / spec.weights_file + ) + return cache / spec.provider / spec.filename + + +def model_artifact_cached( + cache: Path, + spec: ModelSpec | ArtifactSpec, +) -> bool: + return model_artifact_path(cache, spec).is_file() diff --git a/src/vidxp/ports.py b/src/vidxp/ports.py new file mode 100644 index 0000000..4365800 --- /dev/null +++ b/src/vidxp/ports.py @@ -0,0 +1,413 @@ +from __future__ import annotations + +from pathlib import Path +from typing import ( + Any, + Callable, + ContextManager, + Iterable, + Mapping, + Protocol, + runtime_checkable, +) + +from vidxp.application_models import ( + DraftAnswer, + Job, + JobPage, + JobQueue, + JobRequest, + ListJobsCommand, + QueryModelIdentity, + QueryPlan, + QueryPlanningRequest, + QuerySynthesisRequest, + RuntimeProfile, +) +from vidxp.core.artifacts import ( + ArtifactRecord, + StagedArtifact, + StoredArtifact, +) +from vidxp.core.contracts import ( + CancellationToken, + IndexConfig, + StorageRecord, +) +from vidxp.core.indexing_common import ProgressCallback +from vidxp.core.media import ( + MediaProbe, + MediaRecord, + StagedMedia, + StoredMedia, +) +from vidxp.model_contracts import ArtifactSpec, ModelKey, ModelSpec + + +class InvalidJobBackendRequestError(ValueError): + """Raised when a durable job identifier or cursor is malformed.""" + + +class JobIdempotencyConflictError(RuntimeError): + """Raised when one workflow ID is reused for a different request.""" + + +class QueryProviderError(RuntimeError): + """Raised when the optional language-model provider cannot respond.""" + + +@runtime_checkable +class QueryModelPort(Protocol): + @property + def identity(self) -> QueryModelIdentity: ... + + def plan(self, request: QueryPlanningRequest) -> QueryPlan: ... + + def synthesize(self, request: QuerySynthesisRequest) -> DraftAnswer: ... + + +class LocalFileResource: + """Authorized local delivery handle; never serialize this object.""" + + __slots__ = ("path", "filename", "mime_type", "byte_size", "etag") + + def __init__( + self, + *, + path: Path, + filename: str, + mime_type: str, + byte_size: int, + etag: str, + ) -> None: + self.path = path + self.filename = filename + self.mime_type = mime_type + self.byte_size = byte_size + self.etag = etag + + +@runtime_checkable +class MediaCatalogPort(Protocol): + def get_media(self, media_id: str) -> MediaRecord | None: ... + + def get_media_by_checksum(self, sha256: str) -> MediaRecord | None: ... + + def put_media(self, record: MediaRecord) -> MediaRecord: ... + + def list_media( + self, + *, + limit: int, + offset: int = 0, + ) -> tuple[MediaRecord, ...]: ... + + def count_media(self) -> int: ... + + def reserve_media_import( + self, + request_key: str, + request_fingerprint: str, + ) -> MediaRecord | None: ... + + def complete_media_import( + self, + request_key: str, + request_fingerprint: str, + record: MediaRecord, + ) -> None: ... + + +@runtime_checkable +class MediaStorePort(Protocol): + def stage_local(self, path: Path) -> StagedMedia: ... + + def publication_lock(self, sha256: str) -> ContextManager[None]: ... + + def publish(self, staged: StagedMedia) -> StoredMedia: ... + + def discard(self, staged: StagedMedia) -> None: ... + + def delete(self, storage_key: str) -> None: ... + + def verify( + self, + storage_key: str, + *, + sha256: str, + byte_size: int, + ) -> Path: ... + + def resolve(self, storage_key: str) -> Path: ... + + +@runtime_checkable +class MediaProbePort(Protocol): + def probe(self, path: Path) -> MediaProbe: ... + + +@runtime_checkable +class ArtifactCatalogPort(Protocol): + def get_artifact(self, artifact_id: str) -> ArtifactRecord | None: ... + + def get_artifact_by_request( + self, + request_key: str, + ) -> ArtifactRecord | None: ... + + def invalidate_artifact_request( + self, + request_key: str, + artifact_id: str, + ) -> None: ... + + def put_artifact(self, record: ArtifactRecord) -> ArtifactRecord: ... + + +@runtime_checkable +class ArtifactStorePort(Protocol): + def stage(self, artifact_id: str, *, suffix: str) -> StagedArtifact: ... + + def recover( + self, + artifact_id: str, + *, + suffix: str, + ) -> StoredArtifact | None: ... + + def publish(self, staged: StagedArtifact) -> StoredArtifact: ... + + def discard(self, staged: StagedArtifact) -> None: ... + + def delete(self, storage_key: str) -> None: ... + + def verify( + self, + storage_key: str, + *, + sha256: str, + byte_size: int, + ) -> Path: ... + + def resolve(self, storage_key: str) -> Path: ... + + +@runtime_checkable +class ActorRendererPort(Protocol): + def render( + self, + source: Path, + destination: Path, + cluster_id: str, + detections: list[dict[str, Any]], + *, + cancellation: CancellationToken, + progress: ProgressCallback | None, + ) -> None: ... + + +@runtime_checkable +class SnippetRendererPort(Protocol): + def render( + self, + source: Path, + destination: Path, + *, + start_seconds: float, + end_seconds: float, + compatible_mp4: bool, + cancellation: CancellationToken, + progress: ProgressCallback | None, + ) -> None: ... + + +@runtime_checkable +class ResourceSchedulerPort(Protocol): + def indexing(self): ... + + def inference(self): ... + + +@runtime_checkable +class ModelRuntimePort(Protocol): + backends: RuntimeProfile + scheduler: ResourceSchedulerPort + + @property + def model_cache(self) -> Path: ... + + @property + def cpu_thread_budget(self) -> int: ... + + def device_for(self, capability: str) -> str: ... + + def get_or_load( + self, + key: ModelKey, + loader: Callable[[], Any], + ) -> Any: ... + + def resolve_model( + self, + spec: ModelSpec, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, + ) -> Path: ... + + def resolve_artifact( + self, + spec: ArtifactSpec, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, + ) -> Path: ... + + def record_compute_precision( + self, + capability: str, + precision: str, + ) -> None: ... + + def describe(self) -> dict[str, Any]: ... + + +@runtime_checkable +class IndexReader(Protocol): + """Read-only vector records available to application queries.""" + + def size_bytes(self) -> int | None: ... + + def query( + self, + modality: str, + embedding: list[float], + *, + top_k: int, + video_id: str | None = None, + filters: Mapping[str, Any] | None = None, + ) -> list[dict[str, Any]]: ... + + def records( + self, + modality: str, + *, + video_id: str | None = None, + filters: Mapping[str, Any] | None = None, + limit: int | None = None, + offset: int = 0, + ) -> list[dict[str, Any]]: ... + + def count_records( + self, + modality: str, + *, + video_id: str | None = None, + filters: Mapping[str, Any] | None = None, + ) -> int: ... + + +@runtime_checkable +class IndexStore(IndexReader, Protocol): + """Mutable vector records available only to indexing infrastructure.""" + + def clear(self, modalities: Iterable[str] | None = None) -> None: ... + + def delete_video(self, modality: str, video_id: str) -> None: ... + + def delete_generation( + self, + generation_id: str, + modalities: Iterable[str] | None = None, + ) -> None: ... + + def delete_records( + self, + modality: str, + *, + video_id: str, + filters: Mapping[str, Any] | None = None, + ) -> None: ... + + def upsert( + self, + modality: str, + records: list[StorageRecord], + *, + batch_size: int, + cancellation: CancellationToken, + ) -> int: ... + +class IndexBackend(Protocol): + """Infrastructure operations needed by the application layer.""" + + def status(self, index_directory: Path) -> dict[str, Any] | None: ... + + def active_config( + self, + index_directory: Path, + *, + device: str, + ) -> IndexConfig: ... + + def config_for_snapshot( + self, + index_directory: Path, + *, + snapshot_id: str, + snapshot_sha256: str, + device: str, + ) -> IndexConfig: ... + + def create( + self, + path: Path, + *, + config: IndexConfig, + progress: ProgressCallback | None, + cancellation: CancellationToken | None, + source_name: str | None, + source_checksum: str, + operation_id: str | None = None, + ) -> dict[str, Any]: ... + + def indexing_in_progress(self, config: IndexConfig) -> bool: ... + + def open_store(self, config: IndexConfig) -> ContextManager[IndexReader]: ... + + def remove(self, config: IndexConfig, media_id: str) -> bool: ... + + def clear(self, config: IndexConfig) -> bool: ... + + +class JobBackend(Protocol): + """Durable lifecycle operations owned by the workflow engine.""" + + def start(self) -> None: ... + + def submit( + self, + request: JobRequest, + *, + queue: JobQueue, + job_id: str | None = None, + ) -> Job: ... + + def get(self, job_id: str) -> Job | None: ... + + def list(self, command: ListJobsCommand) -> JobPage: ... + + def cancel(self, job_id: str) -> Job | None: ... + + def retry( + self, + job_id: str, + *, + retry_id: str | None = None, + ) -> Job | None: ... + + def health(self) -> None: ... + + def stop_worker(self) -> bool: ... + + def close(self) -> None: ... diff --git a/src/vidxp/query_service.py b/src/vidxp/query_service.py new file mode 100644 index 0000000..c8d0aaf --- /dev/null +++ b/src/vidxp/query_service.py @@ -0,0 +1,291 @@ +from __future__ import annotations + +import hashlib +import json + +from vidxp.application_models import ( + ActorEvidence, + DraftAnswer, + Evidence, + FusedSearchResult, + GroundedClaim, + IndexSnapshotReference, + MomentEvidence, + QueryAnswer, + QueryAnswerMode, + QueryPlan, + QueryPlanningRequest, + QuerySynthesisRequest, + QueryVideoCommand, + SearchMomentsPlanStep, + ActorOverviewPlanStep, +) +from vidxp.capabilities.actor.schemas import ActorClusterSummary +from vidxp.ports import QueryModelPort, QueryProviderError + + +def _evidence_id(payload: dict[str, object]) -> str: + encoded = json.dumps( + payload, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def _default_plan( + command: QueryVideoCommand, + *, + search_modalities: tuple[str, ...], + actor_overview: bool, +) -> QueryPlan: + steps = [ + SearchMomentsPlanStep( + modality=modality, + query=command.question, + ) + for modality in search_modalities + ] + if actor_overview: + steps.append(ActorOverviewPlanStep()) + return QueryPlan(steps=tuple(steps)) + + +def _valid_plan( + plan: QueryPlan, + *, + search_modalities: tuple[str, ...], + actor_overview: bool, +) -> bool: + searches = [ + step.modality + for step in plan.steps + if isinstance(step, SearchMomentsPlanStep) + ] + actor_steps = sum( + isinstance(step, ActorOverviewPlanStep) for step in plan.steps + ) + return ( + len(searches) == len(set(searches)) + and set(searches) == set(search_modalities) + and actor_steps == int(actor_overview) + ) + + +class GroundedQueryService: + """Apply bounded model assistance to application-owned retrieval evidence.""" + + def __init__(self, model: QueryModelPort | None = None) -> None: + self.model = model + + def plan( + self, + command: QueryVideoCommand, + *, + search_modalities: tuple[str, ...], + actor_overview: bool, + ) -> tuple[QueryPlan, str | None]: + fallback = _default_plan( + command, + search_modalities=search_modalities, + actor_overview=actor_overview, + ) + if self.model is None: + return fallback, "query_model_not_configured" + try: + proposed = self.model.plan( + QueryPlanningRequest( + question=command.question, + allowed_modalities=search_modalities, + actor_overview_allowed=actor_overview, + ) + ) + except QueryProviderError: + return fallback, "query_model_unavailable" + if not _valid_plan( + proposed, + search_modalities=search_modalities, + actor_overview=actor_overview, + ): + return fallback, "query_plan_rejected" + return proposed, None + + @staticmethod + def evidence( + *, + snapshot: IndexSnapshotReference, + fused: FusedSearchResult, + actors: tuple[ActorClusterSummary, ...], + ) -> tuple[Evidence, ...]: + items: list[Evidence] = [] + evidence_ids: set[str] = set() + for moment in fused.moments: + for hit in moment.hits: + display_text = hit.metadata.get("text") + if not isinstance(display_text, str) or not display_text.strip(): + display_text = None + identity = { + "kind": "moment", + "snapshot_id": snapshot.snapshot_id, + "media_id": hit.media_id, + "generation_id": hit.generation_id, + "modality": hit.modality, + "source_id": hit.source_id, + "start": hit.start, + "end": hit.end, + } + evidence_id = _evidence_id(identity) + if evidence_id in evidence_ids: + continue + evidence_ids.add(evidence_id) + items.append( + MomentEvidence( + evidence_id=evidence_id, + snapshot_id=snapshot.snapshot_id, + display_text=display_text, + hit=hit, + **{ + key: value + for key, value in identity.items() + if key not in {"kind", "snapshot_id"} + }, + ) + ) + if len(items) == 200: + return tuple(items) + for actor in actors: + identity = { + "kind": "actor", + "snapshot_id": snapshot.snapshot_id, + "media_id": actor.media_id, + "generation_id": actor.generation_id, + "cluster_id": actor.cluster_id, + "start": actor.first_timestamp, + "end": actor.last_timestamp, + } + evidence_id = _evidence_id(identity) + if evidence_id in evidence_ids: + continue + evidence_ids.add(evidence_id) + items.append( + ActorEvidence( + evidence_id=evidence_id, + snapshot_id=snapshot.snapshot_id, + media_id=actor.media_id, + generation_id=actor.generation_id, + cluster_id=actor.cluster_id, + start=actor.first_timestamp, + end=actor.last_timestamp, + detection_count=actor.detection_count, + display_text=( + f"Actor cluster {actor.cluster_id} appears " + f"{actor.detection_count} times from " + f"{actor.first_timestamp:.3f}s to " + f"{actor.last_timestamp:.3f}s." + ), + ) + ) + if len(items) == 200: + break + return tuple(items) + + def answer( + self, + command: QueryVideoCommand, + *, + plan: QueryPlan, + planning_fallback: str | None, + evidence: tuple[Evidence, ...], + fused: FusedSearchResult, + ) -> QueryAnswer: + common = { + "question": command.question, + "plan": plan, + "model": self.model.identity if self.model is not None else None, + "evidence": evidence, + "moments": fused.moments, + "fusion": fused.fusion, + } + if not evidence: + return QueryAnswer( + mode=QueryAnswerMode.no_evidence, + fallback_reason="no_evidence", + **common, + ) + + citable = tuple( + item + for item in evidence + if isinstance(item, ActorEvidence) + or ( + isinstance(item, MomentEvidence) + and item.display_text is not None + ) + )[:200] + if self.model is None: + return QueryAnswer( + mode=QueryAnswerMode.evidence_only, + fallback_reason=planning_fallback, + **common, + ) + if planning_fallback == "query_model_unavailable": + return QueryAnswer( + mode=QueryAnswerMode.evidence_only, + fallback_reason=planning_fallback, + **common, + ) + if not citable: + return QueryAnswer( + mode=QueryAnswerMode.evidence_only, + fallback_reason="no_textual_evidence", + **common, + ) + try: + draft = self.model.synthesize( + QuerySynthesisRequest( + question=command.question, + evidence=citable, + ) + ) + except QueryProviderError: + return QueryAnswer( + mode=QueryAnswerMode.evidence_only, + fallback_reason="query_model_unavailable", + **common, + ) + claims = self._validated_claims(draft, citable) + if claims is None: + return QueryAnswer( + mode=QueryAnswerMode.evidence_only, + fallback_reason="query_citations_rejected", + **common, + ) + return QueryAnswer( + mode=QueryAnswerMode.generated, + claims=claims, + fallback_reason=planning_fallback, + **common, + ) + + @staticmethod + def _validated_claims( + draft: DraftAnswer, + evidence: tuple[Evidence, ...], + ) -> tuple[GroundedClaim, ...] | None: + allowed = {item.evidence_id for item in evidence} + claims = tuple( + GroundedClaim( + text=claim.text, + evidence_ids=claim.evidence_ids, + ) + for claim in draft.claims + ) + cited = { + evidence_id + for claim in claims + for evidence_id in claim.evidence_ids + } + if not cited.issubset(allowed): + return None + return claims diff --git a/src/vidxp/read_job_planner.py b/src/vidxp/read_job_planner.py new file mode 100644 index 0000000..19079b8 --- /dev/null +++ b/src/vidxp/read_job_planner.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from vidxp.application_boundary import application_boundary +from vidxp.application_models import ( + ActorOverlayJobRequest, + CreateActorOverlayCommand, + IndexSnapshotReference, + QueryJobRequest, + QueryVideoCommand, + SearchCommand, + SearchJobRequest, +) +from vidxp.capabilities.contracts import CapabilityRequestError +from vidxp.infrastructure.local_index import LocalIndexReader +from vidxp.repository_layout import RepositoryLayout + + +class LocalReadJobPlanner: + """Bind durable reads to one immutable snapshot without loading models.""" + + def __init__( + self, + *, + layout: RepositoryLayout, + index: LocalIndexReader | None = None, + ) -> None: + self.layout = layout + self.index = index or LocalIndexReader(layout) + + def _active(self): + config = self.index.active_config( + self.layout.indexes, + device="cpu", + ) + if config.snapshot_id is None or config.snapshot_sha256 is None: + raise RuntimeError( + "The active index did not provide an immutable reference." + ) + return config, IndexSnapshotReference( + snapshot_id=config.snapshot_id, + snapshot_sha256=config.snapshot_sha256, + ) + + @staticmethod + def _require_capability(capability: str, config) -> None: + if capability not in config.enabled_modalities: + raise CapabilityRequestError( + f"The {capability} capability is not present in this index." + ) + + @application_boundary + def plan_search(self, command: SearchCommand) -> SearchJobRequest: + config, reference = self._active() + for capability in command.modalities: + self._require_capability(capability, config) + return SearchJobRequest(command=command, snapshot=reference) + + @application_boundary + def plan_query(self, command: QueryVideoCommand) -> QueryJobRequest: + config, reference = self._active() + for capability in command.modalities: + self._require_capability(capability, config) + return QueryJobRequest(command=command, snapshot=reference) + + @application_boundary + def plan_actor_overlay( + self, + command: CreateActorOverlayCommand, + ) -> ActorOverlayJobRequest: + config, reference = self._active() + self._require_capability("actor", config) + return ActorOverlayJobRequest( + command=command, + snapshot=reference, + ) diff --git a/src/vidxp/readiness_service.py b/src/vidxp/readiness_service.py new file mode 100644 index 0000000..4b539cc --- /dev/null +++ b/src/vidxp/readiness_service.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from vidxp.application_models import ComponentReadiness, RuntimeReadiness +from vidxp.authentication import Authenticator +from vidxp.control_plane import ControlPlaneApplication +from vidxp.job_service import JobService + + +class ReadinessService: + """Transport-neutral aggregate health over injected application services.""" + + def __init__( + self, + *, + application: ControlPlaneApplication, + jobs: JobService, + authenticator: Authenticator, + ) -> None: + self.application = application + self.jobs = jobs + self.authenticator = authenticator + + def details(self) -> RuntimeReadiness: + try: + application = self.application.runtime_readiness() + except Exception: + application = RuntimeReadiness( + ready=False, + runtime=None, + components=( + ComponentReadiness( + name="application", + ready=False, + message="Application readiness is unavailable.", + ), + ), + dependencies=None, + ) + components = self._components(application.components) + return application.model_copy( + update={ + "ready": all(component.ready for component in components), + "components": tuple(components), + } + ) + + def ready(self) -> bool: + try: + application = self.application.control_plane_readiness() + except Exception: + application = ( + ComponentReadiness( + name="application", + ready=False, + message="Application readiness is unavailable.", + ), + ) + return all( + component.ready + for component in self._components(application) + ) + + def _components( + self, + application: tuple[ComponentReadiness, ...], + ) -> list[ComponentReadiness]: + components = list(application) + try: + components.append(self.authenticator.readiness()) + except Exception: + components.append( + ComponentReadiness( + name="authentication", + ready=False, + message="Authentication readiness is unavailable.", + ) + ) + try: + components.append(self.jobs.readiness()) + except Exception: + components.append( + ComponentReadiness( + name="workflow", + ready=False, + message="The durable workflow database is unavailable.", + ) + ) + return components diff --git a/src/vidxp/repositories.py b/src/vidxp/repositories.py index b0e4df7..db03dcf 100644 --- a/src/vidxp/repositories.py +++ b/src/vidxp/repositories.py @@ -3,17 +3,21 @@ import json import os import re -import sys from dataclasses import dataclass from pathlib import Path from typing import Any, Mapping from filelock import FileLock +from vidxp.app_paths import ( + default_config_directory, + default_data_directory, + default_repository_directory, +) REPOSITORY_SCHEMA_VERSION = 1 DEFAULT_REPOSITORY_NAME = "default" -DEFAULT_INDEX_DIRECTORY = Path("chroma_data") +DEFAULT_INDEX_DIRECTORY = default_repository_directory() _NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") @@ -25,15 +29,7 @@ def default_config_path() -> Path: configured = os.environ.get("VIDXP_CONFIG_FILE") if configured: return Path(configured).expanduser() - if sys.platform == "win32": - root = Path(os.environ.get("APPDATA", Path.home() / "AppData/Roaming")) - elif sys.platform == "darwin": - root = Path.home() / "Library/Application Support" - else: - root = Path( - os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config") - ) - return root / "vidxp" / "repositories.json" + return default_config_directory() / "repositories.json" def _repository_name(value: str) -> str: @@ -74,8 +70,18 @@ def to_dict(self) -> dict[str, Any]: class RepositoryRegistry: - def __init__(self, path: str | Path | None = None) -> None: + def __init__( + self, + path: str | Path | None = None, + *, + default_index_directory: str | Path | None = None, + ) -> None: self.path = Path(path) if path is not None else default_config_path() + self.default_index_directory = Path( + default_index_directory + if default_index_directory is not None + else default_repository_directory() + ).expanduser() def read(self) -> dict[str, Any]: return self._read_unlocked() @@ -115,7 +121,7 @@ def list(self) -> tuple[RepositoryConfig, ...]: return ( RepositoryConfig( DEFAULT_REPOSITORY_NAME, - DEFAULT_INDEX_DIRECTORY, + self.default_index_directory, configured=False, ), *configured, @@ -134,7 +140,7 @@ def resolve(self, name: str | None = None) -> RepositoryConfig: if selected == DEFAULT_REPOSITORY_NAME: return RepositoryConfig( DEFAULT_REPOSITORY_NAME, - DEFAULT_INDEX_DIRECTORY, + self.default_index_directory, configured=False, ) raise RepositoryConfigError( @@ -193,7 +199,7 @@ def use(self, name: str) -> RepositoryConfig: elif selected == DEFAULT_REPOSITORY_NAME: repository = RepositoryConfig( DEFAULT_REPOSITORY_NAME, - DEFAULT_INDEX_DIRECTORY, + self.default_index_directory, configured=False, ) payload["active_repository"] = None @@ -275,9 +281,20 @@ def resolve_repository( registry_path: str | Path | None = None, name: str | None = None, index_directory: str | Path | None = None, + data_directory: str | Path | None = None, device: str | None = None, ) -> tuple[RepositoryRegistry, RepositoryConfig]: - registry = RepositoryRegistry(registry_path) + active_data_directory = Path( + data_directory + if data_directory is not None + else os.environ.get("VIDXP_DATA_DIR") or default_data_directory() + ).expanduser() + registry = RepositoryRegistry( + registry_path, + default_index_directory=default_repository_directory( + active_data_directory + ), + ) selected_name = name or os.environ.get("VIDXP_REPOSITORY") repository = registry.resolve(selected_name) resolved_index = ( @@ -285,8 +302,12 @@ def resolve_repository( if index_directory is not None else Path(os.environ["VIDXP_INDEX_DIR"]) if os.environ.get("VIDXP_INDEX_DIR") + else default_repository_directory(active_data_directory) + if not repository.configured else repository.index_directory ) + if not repository.configured: + registry.default_index_directory = resolved_index.expanduser() resolved_device = ( device if device is not None diff --git a/src/vidxp/repository_layout.py b/src/vidxp/repository_layout.py new file mode 100644 index 0000000..5014c87 --- /dev/null +++ b/src/vidxp/repository_layout.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from pathlib import Path + +from pydantic import BaseModel, ConfigDict + + +class RepositoryLayout(BaseModel): + model_config = ConfigDict(frozen=True) + + root: Path + + @property + def descriptor(self) -> Path: + return self.root / "repository.json" + + @property + def media(self) -> Path: + return self.root / "media" + + @property + def catalog(self) -> Path: + return self.root / "catalog.sqlite3" + + @property + def media_objects(self) -> Path: + return self.media / "objects" + + @property + def indexes(self) -> Path: + return self.root / "indexes" + + @property + def local_index(self) -> Path: + return self.indexes + + @property + def generations(self) -> Path: + return self.indexes / "generations" + + @property + def index_store(self) -> Path: + return self.indexes / "store" + + @property + def snapshots(self) -> Path: + return self.indexes / "snapshots" + + @property + def active_snapshot(self) -> Path: + return self.indexes / "active-snapshot.json" + + @property + def index_lease(self) -> Path: + return self.indexes / ".repository.lock" + + @property + def artifacts(self) -> Path: + return self.root / "artifacts" + + @property + def artifact_objects(self) -> Path: + return self.artifacts / "objects" + + @property + def local_workflows(self) -> Path: + return self.root / "local-workflows" + + @property + def workflow_database(self) -> Path: + return self.local_workflows / "jobs.sqlite3" + + def ensure_local_directories(self) -> None: + for path in ( + self.root, + self.media, + self.indexes, + self.index_store, + self.generations, + self.snapshots, + self.artifacts, + self.local_workflows, + ): + path.mkdir(parents=True, exist_ok=True) diff --git a/src/vidxp/requirements/frontend.txt b/src/vidxp/requirements/frontend.txt index c8038a5..116dc32 100644 --- a/src/vidxp/requirements/frontend.txt +++ b/src/vidxp/requirements/frontend.txt @@ -1 +1 @@ -streamlit>=1.37 +streamlit>=1.60,<2 diff --git a/src/vidxp/requirements/mcp.txt b/src/vidxp/requirements/mcp.txt new file mode 100644 index 0000000..12a0a3a --- /dev/null +++ b/src/vidxp/requirements/mcp.txt @@ -0,0 +1 @@ +mcp>=2.0,<3 diff --git a/src/vidxp/requirements/server-storage.txt b/src/vidxp/requirements/server-storage.txt new file mode 100644 index 0000000..dfb0435 --- /dev/null +++ b/src/vidxp/requirements/server-storage.txt @@ -0,0 +1 @@ +chromadb-client>=1.5.9,<2 diff --git a/src/vidxp/requirements/server.txt b/src/vidxp/requirements/server.txt new file mode 100644 index 0000000..9810ff7 --- /dev/null +++ b/src/vidxp/requirements/server.txt @@ -0,0 +1,8 @@ +asgi-correlation-id>=5.0.1,<6 +alembic>=1.18.5,<2 +fastapi>=0.140.13,<0.141 +psutil>=7.2.2,<8 +psycopg[binary]>=3.3.4,<4 +pyjwt[crypto]>=2.13,<3 +python-multipart>=0.0.32,<0.1 +uvicorn[standard]>=0.51,<0.52 diff --git a/src/vidxp/requirements/slm.txt b/src/vidxp/requirements/slm.txt new file mode 100644 index 0000000..d4d765f --- /dev/null +++ b/src/vidxp/requirements/slm.txt @@ -0,0 +1 @@ +pydantic-ai-slim[openai]>=2.13,<3 diff --git a/src/vidxp/requirements/storage.txt b/src/vidxp/requirements/storage.txt index 99812b1..963f2c1 100644 --- a/src/vidxp/requirements/storage.txt +++ b/src/vidxp/requirements/storage.txt @@ -1 +1,2 @@ -chromadb +chromadb>=1.5.9,<2 +psutil>=7.2.2,<8 diff --git a/src/vidxp/requirements/test.txt b/src/vidxp/requirements/test.txt new file mode 100644 index 0000000..f1de72e --- /dev/null +++ b/src/vidxp/requirements/test.txt @@ -0,0 +1,2 @@ +httpx>=0.28.1,<0.29 +pytest>=9.1.1,<10 diff --git a/src/vidxp/runtime.py b/src/vidxp/runtime.py new file mode 100644 index 0000000..4fe70d7 --- /dev/null +++ b/src/vidxp/runtime.py @@ -0,0 +1,407 @@ +from __future__ import annotations + +import platform +import hashlib +from collections import OrderedDict +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout +from contextlib import contextmanager +from threading import BoundedSemaphore, Lock, RLock +from time import monotonic +from typing import Any, Callable, Iterator +from pathlib import Path + +from vidxp.application_models import RuntimeProfile +from vidxp.model_contracts import ( + ArtifactSpec, + ModelArtifactUnavailableError, + ModelKey, + ModelSpec, +) +from vidxp.settings import VidXPSettings + + +class RuntimeBackendUnavailableError(RuntimeError): + """Raised when an explicitly requested compute backend cannot be used.""" + + +def _torch_accelerators() -> tuple[bool, bool]: + try: + import torch + except ModuleNotFoundError: + return False, False + mps = bool( + getattr(torch.backends, "mps", None) + and torch.backends.mps.is_available() + ) + return mps, bool(torch.cuda.is_available()) + + +def resolve_backends(requested: str) -> RuntimeProfile: + mps_available, cuda_available = _torch_accelerators() + normalized = requested.lower() + if normalized == "auto": + torch_device = "cpu" + elif normalized == "mps": + if not mps_available: + raise RuntimeBackendUnavailableError( + "MPS was requested but is unavailable." + ) + torch_device = "mps" + elif normalized.startswith("cuda"): + if not cuda_available: + raise RuntimeBackendUnavailableError( + "CUDA was requested but is unavailable." + ) + if ":" in normalized: + import torch + + index = int(normalized.split(":", 1)[1]) + if index >= torch.cuda.device_count(): + raise RuntimeBackendUnavailableError( + f"CUDA device {index} was requested but only " + f"{torch.cuda.device_count()} device(s) are available." + ) + torch_device = normalized + elif normalized == "cpu": + torch_device = "cpu" + else: + raise RuntimeBackendUnavailableError( + f"Unsupported runtime backend: {requested!r}." + ) + return RuntimeProfile( + requested=normalized, + torch_device=torch_device, + transcription_device=( + normalized if normalized.startswith("cuda") else "cpu" + ), + mps_available=mps_available, + cuda_available=cuda_available, + ) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +class ResourceScheduler: + """Bound concurrent model work without owning workflow state.""" + + def __init__( + self, + *, + indexing_slots: int, + inference_slots: int, + ) -> None: + self._indexing = BoundedSemaphore(indexing_slots) + self._inference = BoundedSemaphore(inference_slots) + + @contextmanager + def indexing(self) -> Iterator[None]: + with self._indexing: + yield + + @contextmanager + def inference(self) -> Iterator[None]: + with self._inference: + yield + + +class ModelRuntime: + """One injected model cache and backend resolver for all capabilities.""" + + def __init__( + self, + settings: VidXPSettings, + *, + allowed_specs: tuple[ModelSpec | ArtifactSpec, ...] = (), + ) -> None: + self.settings = settings + self._allowed_specs = frozenset(allowed_specs) + self.backends = resolve_backends(settings.runtime_backend) + self.scheduler = ResourceScheduler( + indexing_slots=settings.max_concurrent_indexing, + inference_slots=settings.max_concurrent_inference, + ) + self._resources: OrderedDict[ModelKey, Any] = OrderedDict() + self._resolved_models: dict[str, dict[str, Any]] = {} + self._compute_precision: dict[str, str] = {} + self._load_locks: dict[ModelKey, Lock] = {} + self._lock = RLock() + + @property + def model_cache(self) -> Path: + return self.settings.model_cache + + @property + def cpu_thread_budget(self) -> int: + return self.settings.cpu_thread_budget + + def _configure_cpu_threads(self) -> None: + try: + import torch + except ModuleNotFoundError: + pass + else: + torch.set_num_threads(self.cpu_thread_budget) + try: + import cv2 + except ModuleNotFoundError: + pass + else: + cv2.setNumThreads(self.cpu_thread_budget) + + def device_for(self, capability: str) -> str: + if capability == "dialogue.transcription": + return self.backends.transcription_device + if capability == "actor": + return self.backends.actor_device + return self.backends.torch_device + + @staticmethod + def _download_snapshot( + spec: ModelSpec, + *, + cache: Path, + progress: Callable[[dict[str, Any]], None] | None, + ) -> Path: + from huggingface_hub import constants, snapshot_download + from tqdm.auto import tqdm + + # Xet can remain parked at zero bytes without surfacing an error. + # The regular HTTP path has bounded read timeouts and reports bytes + # through tqdm, which is required for durable preparation progress. + constants.HF_HUB_DISABLE_XET = True + state_lock = Lock() + state: dict[str, Any] = { + "current": 0, + "total": spec.download_size_bytes, + "message": f"Connecting to download {spec.model_id}.", + } + + class ReportingTqdm(tqdm): + def display(self, msg=None, pos=None) -> None: + return None + + def update(self, n=1): + result = super().update(n) + if self.unit == "B": + with state_lock: + state.update( + { + "current": int(self.n), + "total": ( + int(self.total) + if self.total + else None + ), + "message": f"Downloading {spec.model_id}.", + } + ) + return result + + def download() -> str: + return snapshot_download( + repo_id=spec.model_id, + revision=spec.revision, + cache_dir=str(cache), + local_files_only=False, + tqdm_class=ReportingTqdm, + ) + + reported_at = 0.0 + with ThreadPoolExecutor(max_workers=1) as pool: + future = pool.submit(download) + while True: + try: + snapshot = future.result(timeout=0.5) + if progress is not None: + with state_lock: + event = dict(state) + progress( + { + "state": "preparing", + "stage": "downloading_model", + **event, + } + ) + break + except FutureTimeout: + now = monotonic() + if progress is None or now - reported_at < 1: + continue + with state_lock: + event = dict(state) + progress( + { + "state": "preparing", + "stage": "downloading_model", + **event, + } + ) + reported_at = now + return Path(snapshot) + + def resolve_model( + self, + spec: ModelSpec, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, + ) -> Path: + if spec not in self._allowed_specs: + raise ModelArtifactUnavailableError(spec.capability) + from huggingface_hub import snapshot_download + + try: + try: + snapshot = Path( + snapshot_download( + repo_id=spec.model_id, + revision=spec.revision, + cache_dir=str(self.settings.model_cache), + local_files_only=True, + ) + ) + except Exception: + if not download or not self.settings.allow_model_downloads: + raise ModelArtifactUnavailableError(spec.capability) + snapshot = self._download_snapshot( + spec, + cache=self.settings.model_cache, + progress=progress, + ) + weights = snapshot / spec.weights_file + if not weights.is_file() or _sha256(weights) != spec.weights_sha256: + raise ModelArtifactUnavailableError(spec.capability) + except ModelArtifactUnavailableError: + raise + except Exception as exc: + raise ModelArtifactUnavailableError(spec.capability) from exc + with self._lock: + self._resolved_models[spec.capability] = spec.identity(cached=True) + return snapshot + + def resolve_artifact( + self, + spec: ArtifactSpec, + *, + download: bool = False, + progress: Callable[[dict[str, Any]], None] | None = None, + ) -> Path: + if spec not in self._allowed_specs: + raise ModelArtifactUnavailableError(spec.capability) + try: + destination = self.settings.model_cache / spec.provider + path = destination / spec.filename + if not path.is_file() or _sha256(path) != spec.sha256: + if not download or not self.settings.allow_model_downloads: + raise ModelArtifactUnavailableError(spec.capability) + if progress is not None: + progress( + { + "state": "preparing", + "stage": "downloading_model", + "message": f"Downloading {spec.model_id}.", + } + ) + import pooch + + resolved = Path( + pooch.retrieve( + url=spec.url, + known_hash=f"sha256:{spec.sha256}", + fname=spec.filename, + path=destination, + progressbar=False, + ) + ) + else: + resolved = path + if not resolved.is_file() or _sha256(resolved) != spec.sha256: + raise ModelArtifactUnavailableError(spec.capability) + except ModelArtifactUnavailableError: + raise + except Exception as exc: + raise ModelArtifactUnavailableError(spec.capability) from exc + with self._lock: + self._resolved_models[spec.capability] = spec.identity(cached=True) + return resolved + + def record_compute_precision( + self, + capability: str, + precision: str, + ) -> None: + with self._lock: + self._compute_precision[capability] = precision + + def get_or_load( + self, + key: ModelKey, + loader: Callable[[], Any], + ) -> Any: + with self._lock: + if key in self._resources: + resource = self._resources.pop(key) + self._resources[key] = resource + return resource + key_lock = self._load_locks.setdefault(key, Lock()) + + with key_lock: + with self._lock: + if key in self._resources: + resource = self._resources.pop(key) + self._resources[key] = resource + return resource + self._configure_cpu_threads() + resource = loader() + with self._lock: + existing = self._resources.pop(key, None) + if existing is not None: + return existing + self._resources[key] = resource + while len(self._resources) > self.settings.max_loaded_models: + self._resources.popitem(last=False) + self._load_locks.pop(key, None) + return resource + + def clear(self) -> None: + with self._lock: + self._resources.clear() + self._load_locks.clear() + try: + import torch + except ModuleNotFoundError: + return + if torch.cuda.is_available(): + torch.cuda.empty_cache() + if ( + getattr(torch.backends, "mps", None) + and torch.backends.mps.is_available() + ): + torch.mps.empty_cache() + + def describe(self) -> dict[str, Any]: + return { + "platform": platform.system().lower(), + "architecture": platform.machine().lower(), + **self.backends.model_dump(mode="json"), + "model_cache": str(self.settings.model_cache), + "allow_model_downloads": self.settings.allow_model_downloads, + "limits": { + "max_loaded_models": self.settings.max_loaded_models, + "max_concurrent_indexing": ( + self.settings.max_concurrent_indexing + ), + "max_concurrent_inference": ( + self.settings.max_concurrent_inference + ), + "cpu_thread_budget": self.settings.cpu_thread_budget, + }, + "resolved_models": dict(self._resolved_models), + "compute_precision": dict(self._compute_precision), + } diff --git a/src/vidxp/search_fusion.py b/src/vidxp/search_fusion.py new file mode 100644 index 0000000..b2f4828 --- /dev/null +++ b/src/vidxp/search_fusion.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import hashlib + +from vidxp.application_models import ( + FusedMoment, + FusedSearchResult, + FusionProvenance, + SearchHit, + SearchResult, +) + + +RRF_RANK_CONSTANT = 60 + + +def _query_id( + query: str, + modalities: tuple[str, ...], + media_id: str | None, + atomic_query_ids: tuple[str, ...], +) -> str: + identity = "\0".join( + ( + "rrf_v1", + query, + ",".join(modalities), + media_id or "*", + *atomic_query_ids, + ) + ) + return "fused:" + hashlib.sha256(identity.encode("utf-8")).hexdigest() + + +def _connected_components( + hits: tuple[SearchHit, ...], +) -> list[list[SearchHit]]: + ordered = sorted( + hits, + key=lambda hit: ( + hit.media_id, + hit.start, + hit.end, + hit.modality, + hit.rank, + hit.source_id, + ), + ) + components: list[list[SearchHit]] = [] + current: list[SearchHit] = [] + current_media: str | None = None + current_end = 0.0 + for hit in ordered: + if ( + not current + or hit.media_id != current_media + or hit.start > current_end + ): + if current: + components.append(current) + current = [hit] + current_media = hit.media_id + current_end = hit.end + else: + current.append(hit) + current_end = max(current_end, hit.end) + if current: + components.append(current) + return components + + +def _score(hits: list[SearchHit]) -> float: + best_ranks: dict[str, int] = {} + for hit in hits: + best_ranks[hit.modality] = min( + hit.rank, + best_ranks.get(hit.modality, hit.rank), + ) + return sum( + 1.0 / (RRF_RANK_CONSTANT + rank) + for rank in best_ranks.values() + ) + + +def fuse_search_results( + *, + query: str, + requested_modalities: tuple[str, ...], + results: tuple[SearchResult, ...], + media_id: str | None = None, + top_k: int = 10, +) -> FusedSearchResult: + by_modality = {result.modality: result for result in results} + if len(by_modality) != len(results): + raise ValueError("Fusion accepts one result per modality.") + searched_modalities = tuple( + modality + for modality in requested_modalities + if modality in by_modality + ) + tuple( + sorted(set(by_modality) - set(requested_modalities)) + ) + ordered_results = tuple( + by_modality[modality] for modality in searched_modalities + ) + flattened = tuple( + hit for result in ordered_results for hit in result.hits + ) + candidates = [] + for hits in _connected_components(flattened): + ordered_hits = tuple( + sorted( + hits, + key=lambda hit: ( + hit.modality, + hit.rank, + hit.source_id, + ), + ) + ) + candidates.append( + { + "score": _score(hits), + "media_id": hits[0].media_id, + "start": min(hit.start for hit in hits), + "end": max(hit.end for hit in hits), + "modalities": tuple( + sorted({hit.modality for hit in hits}) + ), + "hits": ordered_hits, + } + ) + candidates.sort( + key=lambda item: ( + -item["score"], + item["media_id"], + item["start"], + item["end"], + tuple(hit.source_id for hit in item["hits"]), + ) + ) + moments = tuple( + FusedMoment(rank=rank, **candidate) + for rank, candidate in enumerate(candidates[:top_k], start=1) + ) + return FusedSearchResult( + query_id=_query_id( + query, + searched_modalities, + media_id, + tuple(result.query_id for result in ordered_results), + ), + query=query, + modalities=searched_modalities, + moments=moments, + fusion=FusionProvenance( + requested_modalities=requested_modalities, + searched_modalities=searched_modalities, + ), + ) diff --git a/src/vidxp/settings.py b/src/vidxp/settings.py new file mode 100644 index 0000000..18a2f1c --- /dev/null +++ b/src/vidxp/settings.py @@ -0,0 +1,673 @@ +from __future__ import annotations + +import os +import re +from enum import StrEnum +from pathlib import Path +from urllib.parse import urlsplit + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + SecretStr, + ValidationInfo, + field_validator, + model_validator, +) +from pydantic_settings import BaseSettings, SettingsConfigDict + +from vidxp.app_paths import ( + default_data_directory, + default_model_directory, + default_repository_directory, +) +from vidxp.media_runtime import default_media_executable +from vidxp.repository_layout import RepositoryLayout + + +class ApplicationMode(StrEnum): + local = "local" + remote = "remote" + server = "server" + + +class HttpAuthMode(StrEnum): + none = "none" + static = "static" + oidc = "oidc" + + +class VidXPSettings(BaseSettings): + model_config = SettingsConfigDict( + env_prefix="VIDXP_", + env_nested_delimiter="__", + extra="ignore", + frozen=True, + ) + + mode: ApplicationMode = ApplicationMode.local + data_dir: Path = Field(default_factory=default_data_directory) + repository_root: Path = Field( + default_factory=default_repository_directory + ) + runtime_backend: str = "auto" + model_cache: Path = Field( + default_factory=default_model_directory + ) + allow_model_downloads: bool = True + max_loaded_models: int = Field(default=3, gt=0, le=16) + max_concurrent_indexing: int = Field(default=1, gt=0, le=16) + max_concurrent_inference: int = Field(default=2, gt=0, le=64) + workflow_poll_interval_seconds: float = Field( + default=0.25, + gt=0, + le=10, + ) + cpu_thread_budget: int = Field( + default_factory=lambda: min(256, max(1, os.cpu_count() or 1)), + gt=0, + le=256, + ) + max_local_import_bytes: int = Field( + default=50 * 1024 * 1024 * 1024, + gt=0, + ) + max_snippet_duration_seconds: float = Field( + default=300, + gt=0, + le=3600, + ) + http_bind_host: str = Field(default="127.0.0.1", min_length=1) + http_port: int = Field(default=8000, gt=0, le=65535) + http_auth_mode: HttpAuthMode = HttpAuthMode.none + http_static_bearer_token: SecretStr | None = None + http_oidc_issuer: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + http_oidc_audience: str | None = Field( + default=None, + min_length=1, + max_length=512, + ) + http_oidc_jwks_url: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + http_oidc_algorithms: tuple[str, ...] = ("RS256",) + http_required_scopes: tuple[str, ...] = () + http_trusted_hosts: tuple[str, ...] = ( + "127.0.0.1", + "::1", + "localhost", + "testserver", + ) + http_allowed_origins: tuple[str, ...] = () + http_max_json_body_bytes: int = Field( + default=4 * 1024 * 1024, + gt=0, + le=16 * 1024 * 1024, + ) + http_max_small_upload_bytes: int = Field( + default=256 * 1024 * 1024, + gt=0, + le=256 * 1024 * 1024, + ) + mcp_public_url: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + mcp_max_request_body_bytes: int = Field( + default=4 * 1024 * 1024, + gt=0, + le=16 * 1024 * 1024, + ) + mcp_allowed_hosts: tuple[str, ...] = ( + "127.0.0.1:*", + "[::1]:*", + "localhost:*", + "testserver", + ) + mcp_allowed_origins: tuple[str, ...] = () + upload_public_endpoint: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + upload_internal_endpoint: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + upload_max_bytes: int = Field( + default=50 * 1024 * 1024 * 1024, + gt=0, + ) + upload_quota_bytes: int = Field( + default=100 * 1024 * 1024 * 1024, + gt=0, + ) + upload_intent_ttl_seconds: int = Field( + default=24 * 60 * 60, + ge=300, + le=7 * 24 * 60 * 60, + ) + upload_recovery_interval_seconds: int = Field( + default=60, + ge=10, + le=3600, + ) + upload_quarantine_root: Path | None = None + upload_cleanup_token: SecretStr | None = None + slm_base_url: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + slm_model: str | None = Field( + default=None, + min_length=1, + max_length=255, + ) + slm_timeout_seconds: float = Field(default=60, gt=0, le=600) + slm_output_retries: int = Field(default=1, ge=0, le=3) + trusted_local_import_roots: tuple[Path, ...] = () + ffprobe_executable: str = Field( + default_factory=lambda: default_media_executable("ffprobe"), + min_length=1, + ) + ffmpeg_executable: str = Field( + default_factory=lambda: default_media_executable("ffmpeg"), + min_length=1, + ) + external_capabilities: bool = False + capability_allowlist: tuple[str, ...] = () + + @model_validator(mode="before") + @classmethod + def _derive_storage_paths(cls, value): + if not isinstance(value, dict): + return value + configured = dict(value) + data_directory = Path( + configured.get("data_dir") or default_data_directory() + ).expanduser() + configured.setdefault("data_dir", data_directory) + configured.setdefault( + "repository_root", + default_repository_directory(data_directory), + ) + configured.setdefault( + "model_cache", + default_model_directory(data_directory), + ) + return configured + + @field_validator("slm_base_url", "slm_model", mode="before") + @classmethod + def _normalize_empty_optional_slm( + cls, + value: str | None, + ) -> str | None: + return None if value == "" else value + + @field_validator("runtime_backend") + @classmethod + def _validate_runtime_backend(cls, value: str) -> str: + backend = value.strip().lower() + if not re.fullmatch(r"(auto|cpu|mps|cuda(?::[0-9]+)?)", backend): + raise ValueError( + "runtime_backend must be auto, cpu, mps, cuda, " + "or cuda:." + ) + return backend + + @field_validator("capability_allowlist") + @classmethod + def _clean_allowlist(cls, values: tuple[str, ...]) -> tuple[str, ...]: + cleaned = tuple( + dict.fromkeys(value.strip() for value in values if value.strip()) + ) + invalid = [ + value + for value in cleaned + if value.count(":") != 1 + or not all(part.strip() for part in value.split(":", 1)) + ] + if invalid: + raise ValueError( + "capability_allowlist entries must use " + "DISTRIBUTION:ENTRY_POINT." + ) + return cleaned + + @field_validator( + "http_required_scopes", + "http_trusted_hosts", + "http_allowed_origins", + "mcp_allowed_hosts", + "mcp_allowed_origins", + ) + @classmethod + def _clean_http_lists(cls, values: tuple[str, ...]) -> tuple[str, ...]: + return tuple( + dict.fromkeys(value.strip() for value in values if value.strip()) + ) + + @field_validator("http_trusted_hosts") + @classmethod + def _validate_trusted_hosts( + cls, + values: tuple[str, ...], + ) -> tuple[str, ...]: + normalized = tuple(value.lower() for value in values) + for value in normalized: + if ( + "*" in value[1:] + or ( + value.startswith("*") + and value != "*" + and not value.startswith("*.") + ) + ): + raise ValueError( + "Trusted-host wildcards must use *.example.com." + ) + return normalized + + @field_validator("mcp_allowed_hosts") + @classmethod + def _validate_mcp_hosts( + cls, + values: tuple[str, ...], + ) -> tuple[str, ...]: + normalized = tuple(value.lower() for value in values) + for value in normalized: + if "*" in value and not value.endswith(":*"): + raise ValueError( + "MCP host wildcards are supported only as host:*." + ) + if "/" in value or "://" in value: + raise ValueError( + "MCP allowed hosts must be Host header values." + ) + return normalized + + @field_validator("mcp_allowed_origins") + @classmethod + def _validate_mcp_origins( + cls, + values: tuple[str, ...], + ) -> tuple[str, ...]: + for value in values: + candidate = value[:-2] if value.endswith(":*") else value + parsed = urlsplit(candidate) + if ( + value == "null" + or parsed.scheme not in {"http", "https"} + or parsed.hostname is None + or parsed.username is not None + or parsed.password is not None + or parsed.path + or parsed.query + or parsed.fragment + ): + raise ValueError( + "MCP allowed origins must be serialized HTTP origins." + ) + try: + parsed.port + except ValueError as exc: + raise ValueError( + "An MCP allowed origin contains an invalid port." + ) from exc + if "*" in value and not value.endswith(":*"): + raise ValueError( + "MCP origin wildcards are supported only as origin:*." + ) + return values + + @field_validator("http_oidc_algorithms") + @classmethod + def _validate_oidc_algorithms( + cls, + values: tuple[str, ...], + ) -> tuple[str, ...]: + allowed = { + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "EdDSA", + } + cleaned = tuple(dict.fromkeys(values)) + if not cleaned or any(value not in allowed for value in cleaned): + raise ValueError( + "http_oidc_algorithms must contain supported asymmetric " + "signature algorithms." + ) + return cleaned + + @field_validator("http_oidc_issuer", "http_oidc_jwks_url") + @classmethod + def _validate_oidc_url( + cls, + value: str | None, + info: ValidationInfo, + ) -> str | None: + if value is None: + return None + if value != value.strip(): + raise ValueError(f"{info.field_name} must not contain whitespace.") + if "\\" in value or any( + character.isspace() + or ord(character) < 32 + or ord(character) == 127 + for character in value + ): + raise ValueError( + f"{info.field_name} contains an unsafe URL character." + ) + parsed = urlsplit(value) + if parsed.scheme not in {"http", "https"} or parsed.hostname is None: + raise ValueError(f"{info.field_name} must be an HTTP URL.") + try: + parsed.port + except ValueError as exc: + raise ValueError( + f"{info.field_name} contains an invalid port." + ) from exc + if parsed.username is not None or parsed.password is not None: + raise ValueError(f"{info.field_name} must not contain credentials.") + if "#" in value: + raise ValueError(f"{info.field_name} must not contain a fragment.") + if parsed.scheme != "https" and parsed.hostname.lower() not in { + "localhost", + "127.0.0.1", + "::1", + }: + raise ValueError( + f"{info.field_name} must use HTTPS outside loopback." + ) + if info.field_name == "http_oidc_issuer" and "?" in value: + raise ValueError("http_oidc_issuer must not contain a query.") + return value + + @field_validator( + "upload_public_endpoint", + "upload_internal_endpoint", + "slm_base_url", + ) + @classmethod + def _validate_service_url( + cls, + value: str | None, + info: ValidationInfo, + ) -> str | None: + if value is None: + return None + if value != value.strip() or "\\" in value: + raise ValueError(f"{info.field_name} contains unsafe characters.") + parsed = urlsplit(value) + if ( + parsed.scheme not in {"http", "https"} + or parsed.hostname is None + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + ): + raise ValueError(f"{info.field_name} must be a plain HTTP URL.") + try: + parsed.port + except ValueError as exc: + raise ValueError( + f"{info.field_name} contains an invalid port." + ) from exc + if ( + info.field_name == "upload_public_endpoint" + and parsed.scheme != "https" + and parsed.hostname.lower() not in { + "localhost", + "127.0.0.1", + "::1", + } + ): + raise ValueError( + "upload_public_endpoint must use HTTPS outside loopback." + ) + if info.field_name == "slm_base_url": + if parsed.path.rstrip("/") != "/v1": + raise ValueError("slm_base_url must end with /v1.") + if parsed.hostname.lower() in {"ollama.com", "www.ollama.com"}: + raise ValueError( + "slm_base_url must use a self-hosted Ollama service." + ) + if ( + info.field_name != "slm_base_url" + and not value.endswith("/") + ): + raise ValueError(f"{info.field_name} must end with a slash.") + return value + + @field_validator("mcp_public_url") + @classmethod + def _validate_mcp_public_url(cls, value: str | None) -> str | None: + if value is None: + return None + if value != value.strip() or "\\" in value: + raise ValueError("mcp_public_url contains unsafe characters.") + parsed = urlsplit(value) + if ( + parsed.scheme not in {"http", "https"} + or parsed.hostname is None + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or parsed.path.rstrip("/") != "/mcp" + ): + raise ValueError( + "mcp_public_url must be a plain HTTP URL ending in /mcp." + ) + if ( + parsed.scheme != "https" + and parsed.hostname.lower() + not in {"localhost", "127.0.0.1", "::1"} + ): + raise ValueError( + "mcp_public_url must use HTTPS outside loopback." + ) + return value.rstrip("/") + + @field_validator("trusted_local_import_roots") + @classmethod + def _clean_import_roots( + cls, + values: tuple[Path, ...], + ) -> tuple[Path, ...]: + return tuple( + dict.fromkeys(path.expanduser() for path in values) + ) + + @model_validator(mode="after") + def _require_explicit_server_backend(self) -> "VidXPSettings": + if self.mode == ApplicationMode.remote: + raise ValueError( + "Remote client mode is not available in this release. " + "Connect agents to the remote MCP endpoint or use the HTTP " + "API directly." + ) + if ( + self.mode == ApplicationMode.server + and not re.fullmatch(r"(cpu|cuda(?::[0-9]+)?)", self.runtime_backend) + ): + raise ValueError( + "Server mode requires an explicit cpu or cuda runtime backend." + ) + if self.http_auth_mode == HttpAuthMode.static: + if self.http_static_bearer_token is None or len( + self.http_static_bearer_token.get_secret_value() + ) < 32: + raise ValueError( + "Static HTTP authentication requires a bearer token of " + "at least 32 characters." + ) + if any( + value is not None + for value in ( + self.http_oidc_issuer, + self.http_oidc_audience, + self.http_oidc_jwks_url, + ) + ): + raise ValueError( + "Static HTTP authentication cannot include OIDC settings." + ) + elif self.http_auth_mode == HttpAuthMode.oidc: + if ( + self.http_oidc_issuer is None + or self.http_oidc_audience is None + or self.http_oidc_jwks_url is None + ): + raise ValueError( + "OIDC HTTP authentication requires issuer, audience, " + "and JWKS URL settings." + ) + if self.http_static_bearer_token is not None: + raise ValueError( + "OIDC HTTP authentication cannot include a static token." + ) + if not self.http_required_scopes: + raise ValueError( + "OIDC HTTP authentication requires at least one scope." + ) + elif self.http_static_bearer_token is not None or any( + value is not None + for value in ( + self.http_oidc_issuer, + self.http_oidc_audience, + self.http_oidc_jwks_url, + ) + ): + raise ValueError( + "HTTP credentials require an explicit authentication mode." + ) + if self.upload_public_endpoint is not None: + if ( + self.upload_internal_endpoint is None + or self.upload_cleanup_token is None + or len(self.upload_cleanup_token.get_secret_value()) < 32 + ): + raise ValueError( + "Remote uploads require an internal tusd endpoint and " + "a cleanup token of at least 32 characters." + ) + if (self.slm_base_url is None) != (self.slm_model is None): + raise ValueError( + "slm_base_url and slm_model must be configured together." + ) + if self.slm_model is not None and self.slm_model.endswith("-cloud"): + raise ValueError("slm_model must be a self-hosted Ollama model.") + return self + + def validate_http_server(self) -> None: + if ( + self.mode == ApplicationMode.server + and self.http_auth_mode == HttpAuthMode.none + ): + raise ValueError( + "Server-mode HTTP requires static bearer or OIDC " + "authentication." + ) + if ( + self.http_auth_mode == HttpAuthMode.none + and self.http_bind_host not in {"127.0.0.1", "::1", "localhost"} + ): + raise ValueError( + "Unauthenticated HTTP may bind only to a loopback address." + ) + if not self.http_trusted_hosts: + raise ValueError("At least one trusted HTTP host is required.") + if not self.mcp_allowed_hosts: + raise ValueError("At least one allowed MCP host is required.") + if ( + self.http_auth_mode == HttpAuthMode.oidc + and self.mcp_public_url is None + ): + raise ValueError( + "OIDC MCP authentication requires mcp_public_url." + ) + + @property + def layout(self) -> RepositoryLayout: + return RepositoryLayout(root=self.repository_root.expanduser()) + + @property + def quarantine_root(self) -> Path: + return ( + self.upload_quarantine_root + if self.upload_quarantine_root is not None + else self.repository_root / "upload-quarantine" + ).expanduser() + + +class LocalExecutionSettings(BaseModel): + """Non-secret settings allowed to cross into local execution processes.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + data_dir: Path + repository_root: Path + runtime_backend: str + model_cache: Path + allow_model_downloads: bool + max_loaded_models: int + max_concurrent_indexing: int + max_concurrent_inference: int + workflow_poll_interval_seconds: float + cpu_thread_budget: int + max_local_import_bytes: int + max_snippet_duration_seconds: float + trusted_local_import_roots: tuple[Path, ...] + ffprobe_executable: str + ffmpeg_executable: str + external_capabilities: bool + capability_allowlist: tuple[str, ...] + slm_base_url: str | None + slm_model: str | None + slm_timeout_seconds: float + slm_output_retries: int + + @classmethod + def from_settings( + cls, + settings: VidXPSettings, + ) -> "LocalExecutionSettings": + return cls( + **settings.model_dump( + include=set(cls.model_fields), + mode="python", + ) + ) + + def application_settings(self) -> VidXPSettings: + defaults = VidXPSettings.model_construct().model_dump(mode="python") + defaults.update(self.model_dump(mode="python")) + defaults["mode"] = ApplicationMode.local + defaults["upload_public_endpoint"] = None + defaults["upload_internal_endpoint"] = None + defaults["upload_cleanup_token"] = None + defaults["http_auth_mode"] = HttpAuthMode.none + defaults["http_static_bearer_token"] = None + defaults["http_oidc_issuer"] = None + defaults["http_oidc_audience"] = None + defaults["http_oidc_jwks_url"] = None + defaults["http_required_scopes"] = () + return VidXPSettings(**defaults) diff --git a/src/vidxp/tusd_hooks.py b/src/vidxp/tusd_hooks.py new file mode 100644 index 0000000..d891b4a --- /dev/null +++ b/src/vidxp/tusd_hooks.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, +) +from vidxp.authentication import Authenticator +from vidxp.authorization import AuthorizationPolicy, RepositoryPermission +from vidxp.infrastructure.tusd_contracts import ( + TusdChangeFileInfo, + TusdHookRequest, + TusdHookResponse, + TusdHTTPResponse, +) +from vidxp.upload_service import RemoteUploadService + + +def _reject( + *, + status: int, + code: str, + message: str, + stop: bool = False, +) -> TusdHookResponse: + return TusdHookResponse( + reject_upload=not stop, + reject_termination=stop, + http_response=TusdHTTPResponse( + status_code=status, + body=message, + headers={"X-VidXP-Error": code}, + ), + ) + + +def _status(error: ApplicationError) -> int: + return { + ErrorCategory.authentication: 401, + ErrorCategory.authorization: 403, + ErrorCategory.not_found: 404, + ErrorCategory.conflict: 409, + ErrorCategory.resource_limit: 429, + ErrorCategory.validation: 400, + ErrorCategory.unavailable: 503, + }.get(error.detail.category, 500) + + +def _bearer(request) -> str | None: + values = request.header("authorization") + if len(values) != 1: + return None + scheme, separator, token = values[0].partition(" ") + if separator != " " or scheme.lower() != "bearer" or not token: + return None + return token + + +class TusdHookService: + """Translate tusd hook events into the upload application service.""" + + def __init__( + self, + *, + uploads: RemoteUploadService, + authenticator: Authenticator, + authorization: AuthorizationPolicy, + ) -> None: + self.uploads = uploads + self.authenticator = authenticator + self.authorization = authorization + + def handle(self, hook: TusdHookRequest) -> TusdHookResponse: + try: + if hook.hook_type == "pre-create": + return self._pre_create(hook) + if hook.hook_type == "post-finish": + self._post_finish(hook) + return TusdHookResponse() + if hook.hook_type == "pre-terminate": + self._pre_terminate(hook) + return TusdHookResponse() + self.uploads.record_terminated(hook.event.upload.upload_id) + return TusdHookResponse() + except ApplicationError as exc: + if hook.hook_type in {"post-finish", "post-terminate"}: + raise + return _reject( + status=_status(exc), + code=exc.detail.code, + message=exc.detail.message, + stop=hook.hook_type == "pre-terminate", + ) + + def _pre_create(self, hook: TusdHookRequest) -> TusdHookResponse: + upload = hook.event.upload + if ( + upload.size_is_deferred + or upload.is_partial + or upload.is_final + or upload.partial_uploads + or set(upload.metadata) != {"intent_id"} + ): + raise ApplicationError( + "upload_protocol_unsupported", + ErrorCategory.validation, + "Deferred-length and concatenated uploads are not supported.", + ) + principal = self.authorization.require( + self.authenticator.authenticate(_bearer(hook.event.request)), + RepositoryPermission.write, + ) + record = self.uploads.accept_creation( + upload.metadata["intent_id"], + principal=principal, + byte_size=upload.size, + ) + assert record.upload_id is not None + return TusdHookResponse( + change_file_info=TusdChangeFileInfo(upload_id=record.upload_id) + ) + + def _post_finish(self, hook: TusdHookRequest) -> None: + upload = hook.event.upload + intent_id = upload.metadata.get("intent_id") + if intent_id is None: + raise ApplicationError( + "upload_completion_invalid", + ErrorCategory.validation, + "The completed upload is missing its intent.", + ) + self.uploads.complete_upload( + intent_id=intent_id, + upload_id=upload.upload_id, + byte_size=upload.size, + offset=upload.offset, + ) + + def _pre_terminate(self, hook: TusdHookRequest) -> None: + values = hook.event.request.header("x-vidxp-cleanup-token") + token = values[0] if len(values) == 1 else None + self.uploads.authorize_termination( + hook.event.upload.upload_id, + cleanup_token=token, + ) diff --git a/src/vidxp/upload_service.py b/src/vidxp/upload_service.py new file mode 100644 index 0000000..2a0bb90 --- /dev/null +++ b/src/vidxp/upload_service.py @@ -0,0 +1,659 @@ +from __future__ import annotations + +import hashlib +import json +import logging +from datetime import timedelta +from hmac import compare_digest +from pathlib import Path +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen +from uuid import uuid4 + +from sqlalchemy.engine import Connection +from sqlalchemy.exc import IntegrityError + +from vidxp.application_models import ( + ApplicationError, + CreateUploadIntentCommand, + ErrorCategory, + JobState, + Principal, + UploadIntent, +) +from vidxp.core.media import QuarantinedMedia, utc_now +from vidxp.core.uploads import UploadIntentRecord, UploadState +from vidxp.infrastructure.sql_catalog import ( + SQLCatalog, + UploadQuotaExceededError, +) +from vidxp.media_service import MediaService +from vidxp.settings import VidXPSettings + +LOGGER = logging.getLogger(__name__) + + +def _public_intent(record: UploadIntentRecord) -> UploadIntent: + return UploadIntent( + intent_id=record.intent_id, + original_filename=record.original_filename, + byte_size=record.byte_size, + declared_mime_type=record.declared_mime_type, + state=record.state, + created_at=record.created_at, + expires_at=record.expires_at, + job_id=record.job_id, + media_id=record.media_id, + ) + + +class RemoteUploadService: + """Intent policy and minimal glue around tusd, DBOS, and media import.""" + + def __init__( + self, + *, + settings: VidXPSettings, + catalog: SQLCatalog, + media: MediaService | None, + jobs: Any | None = None, + ) -> None: + self.settings = settings + self.catalog = catalog + self.media = media + self.jobs = jobs + + def create_intent( + self, + command: CreateUploadIntentCommand, + *, + principal: Principal, + request_key: str, + ) -> UploadIntent: + self._require_configured() + del principal + if command.byte_size > self.settings.upload_max_bytes: + raise ApplicationError( + "upload_too_large", + ErrorCategory.resource_limit, + "The requested upload exceeds the configured limit.", + ) + if existing := self.catalog.get_upload_intent_by_request(request_key): + self._require_same_request(existing, command) + return _public_intent(existing) + now = utc_now() + record = UploadIntentRecord( + intent_id=uuid4().hex, + request_key=request_key, + original_filename=command.original_filename, + byte_size=command.byte_size, + declared_mime_type=command.declared_mime_type, + state=UploadState.pending, + created_at=now, + expires_at=now + + timedelta(seconds=self.settings.upload_intent_ttl_seconds), + ) + try: + return _public_intent( + self.catalog.create_upload_intent( + record, + quota_limit=self.settings.upload_quota_bytes, + ) + ) + except UploadQuotaExceededError as exc: + raise ApplicationError( + "upload_quota_exceeded", + ErrorCategory.resource_limit, + "The principal upload quota would be exceeded.", + ) from exc + except IntegrityError: + replay = self.catalog.get_upload_intent_by_request(request_key) + if replay is not None: + self._require_same_request(replay, command) + return _public_intent(replay) + raise + + def get_intent( + self, + intent_id: str, + *, + principal: Principal, + ) -> UploadIntent: + del principal + record = self.catalog.get_upload_intent(intent_id) + if record is None: + raise ApplicationError( + "resource_not_found", + ErrorCategory.not_found, + "The requested upload intent was not found.", + ) + return _public_intent(record) + + def upload_url( + self, + intent_id: str, + *, + principal: Principal, + ) -> str | None: + del principal + record = self.catalog.get_upload_intent(intent_id) + if record is None: + raise ApplicationError( + "resource_not_found", + ErrorCategory.not_found, + "The requested upload intent was not found.", + ) + if record.state != UploadState.accepted or record.upload_id is None: + return None + assert self.settings.upload_public_endpoint is not None + return self.settings.upload_public_endpoint + record.upload_id + + def accept_creation( + self, + intent_id: str, + *, + principal: Principal, + byte_size: int, + ) -> UploadIntentRecord: + del principal + now = utc_now() + + def accept(connection: Connection) -> UploadIntentRecord | None: + record = self.catalog.get_upload_intent( + intent_id, + connection=connection, + for_update=True, + ) + if record is None: + raise ApplicationError( + "upload_intent_invalid", + ErrorCategory.validation, + "The upload intent is invalid.", + ) + if record.expires_at <= now: + self.catalog.update_upload( + record.intent_id, + state=UploadState.expired, + connection=connection, + ) + return None + if byte_size != record.byte_size: + raise ApplicationError( + "upload_size_mismatch", + ErrorCategory.validation, + "The upload size does not match its intent.", + ) + if record.state == UploadState.accepted: + assert record.upload_id is not None + if self._quarantine_path( + f"{record.upload_id}.info" + ).exists(): + raise ApplicationError( + "upload_already_created", + ErrorCategory.conflict, + "The upload was already created; resume its existing URL.", + ) + return record + if record.state != UploadState.pending: + raise ApplicationError( + "upload_intent_consumed", + ErrorCategory.conflict, + "The upload intent has already been consumed.", + ) + upload_id = uuid4().hex + self.catalog.update_upload( + record.intent_id, + state=UploadState.accepted, + connection=connection, + upload_id=upload_id, + ) + return record.model_copy( + update={ + "state": UploadState.accepted, + "upload_id": upload_id, + } + ) + + accepted = self.catalog.with_upload_transaction(accept) + if accepted is None: + raise ApplicationError( + "upload_intent_expired", + ErrorCategory.validation, + "The upload intent has expired.", + ) + return accepted + + def complete_upload( + self, + *, + intent_id: str, + upload_id: str, + byte_size: int, + offset: int, + ) -> str: + if self.jobs is None: + raise RuntimeError("Upload job submission is not configured.") + + def complete(connection: Connection) -> str: + record = self.catalog.get_upload_intent( + intent_id, + connection=connection, + for_update=True, + ) + if ( + record is None + or record.upload_id != upload_id + ): + raise ApplicationError( + "upload_completion_invalid", + ErrorCategory.validation, + "The completed upload does not match its intent.", + ) + if byte_size != record.byte_size or offset != record.byte_size: + raise ApplicationError( + "upload_incomplete", + ErrorCategory.validation, + "The upload is not complete.", + ) + if record.job_id is not None: + return record.job_id + if record.state != UploadState.accepted: + raise ApplicationError( + "upload_completion_invalid", + ErrorCategory.conflict, + "The upload is not ready for completion.", + ) + job_id = self.jobs.enqueue_media_import_in_transaction( + upload_id, + connection=connection, + job_id=upload_id, + ) + self.catalog.update_upload( + record.intent_id, + state=UploadState.processing, + connection=connection, + job_id=job_id, + ) + return job_id + + return self.catalog.with_upload_transaction(complete) + + def import_completed( + self, + upload_id: str, + ): + if self.media is None: + raise RuntimeError("Media import is not configured in this process.") + record = self.catalog.get_upload_intent_by_upload_id(upload_id) + if record is None: + raise ApplicationError( + "upload_not_found", + ErrorCategory.not_found, + "The completed upload is unavailable.", + ) + if record.state == UploadState.ready and record.media_id is not None: + asset = self.media.get(record.media_id) + self._cleanup_after_import(upload_id) + return asset + if record.state not in { + UploadState.processing, + UploadState.failed, + }: + raise ApplicationError( + "upload_not_ready", + ErrorCategory.conflict, + "The upload is not ready for import.", + ) + path = self._quarantine_path(upload_id) + request_key = hashlib.sha256( + f"vidxp-upload-import-v1\0{record.intent_id}".encode() + ).hexdigest() + try: + asset = self.media.import_quarantined( + QuarantinedMedia( + path=path, + original_filename=record.original_filename, + declared_mime_type=record.declared_mime_type, + ), + request_key=request_key, + ) + except Exception: + self.catalog.with_upload_transaction( + lambda connection: self.catalog.update_upload( + record.intent_id, + state=UploadState.failed, + connection=connection, + ) + ) + raise + self.catalog.with_upload_transaction( + lambda connection: self.catalog.update_upload( + record.intent_id, + state=UploadState.ready, + connection=connection, + media_id=asset.media_id, + ) + ) + self._cleanup_after_import(upload_id) + return asset + + def reconcile(self) -> dict[str, int]: + recovered = 0 + errors = 0 + for record in self.catalog.recoverable_uploads(): + try: + if record.upload_id is None: + continue + info = self._read_upload_info(record.upload_id) + if info is None: + continue + metadata = info.get("MetaData") + if not isinstance(metadata, dict): + continue + if metadata.get("intent_id") != record.intent_id: + continue + size = info.get("Size") + offset = info.get("Offset") + if ( + isinstance(size, int) + and isinstance(offset, int) + and size == offset == record.byte_size + ): + self.complete_upload( + intent_id=record.intent_id, + upload_id=record.upload_id, + byte_size=size, + offset=offset, + ) + recovered += 1 + except Exception: + errors += 1 + LOGGER.exception( + "Upload recovery failed for intent %s.", + record.intent_id, + ) + failed = 0 + if self.jobs is not None: + for record in self.catalog.processing_uploads(): + try: + assert record.job_id is not None + job = self.jobs.get(record.job_id) + if job.state in { + JobState.failed, + JobState.cancelled, + JobState.recovery_exhausted, + }: + changed = self.catalog.with_upload_transaction( + lambda connection, item=record: ( + self.catalog.update_upload( + item.intent_id, + state=UploadState.failed, + connection=connection, + expected_states={ + UploadState.processing, + }, + ) + ) + ) + failed += int(changed) + except Exception: + errors += 1 + LOGGER.exception( + "Upload job reconciliation failed for intent %s.", + record.intent_id, + ) + expired = 0 + now = utc_now() + for record in self.catalog.expired_uploads(now=now): + try: + if ( + record.state == UploadState.accepted + and record.upload_id is not None + and self._upload_is_active(record.upload_id, now=now) + ): + continue + upload_id = self._expire_intent(record.intent_id) + if upload_id is not None: + self._cleanup_upload(upload_id) + expired += 1 + except Exception: + errors += 1 + LOGGER.exception( + "Upload expiration failed for intent %s.", + record.intent_id, + ) + cleaned = 0 + for record in self.catalog.cleanup_uploads(): + assert record.upload_id is not None + try: + self._cleanup_upload(record.upload_id) + cleaned += 1 + except Exception: + errors += 1 + LOGGER.exception( + "Upload cleanup failed for intent %s.", + record.intent_id, + ) + return { + "recovered": recovered, + "expired": expired, + "cleaned": cleaned, + "failed": failed, + "errors": errors, + } + + def authorize_termination( + self, + upload_id: str, + *, + cleanup_token: str | None, + ) -> None: + def authorize(connection: Connection) -> None: + record = self.catalog.get_upload_intent_by_upload_id( + upload_id, + connection=connection, + for_update=True, + ) + if record is None: + raise ApplicationError( + "upload_not_found", + ErrorCategory.not_found, + "The upload is unavailable.", + ) + expected = self.settings.upload_cleanup_token + internal = ( + expected is not None + and cleanup_token is not None + and compare_digest( + expected.get_secret_value(), + cleanup_token, + ) + ) + if record.state == UploadState.accepted: + self.catalog.update_upload( + record.intent_id, + state=UploadState.expired, + connection=connection, + ) + return + if internal and record.state in { + UploadState.ready, + UploadState.failed, + UploadState.expired, + }: + if record.state == UploadState.failed: + self.catalog.update_upload( + record.intent_id, + state=UploadState.expired, + connection=connection, + ) + return + raise ApplicationError( + "upload_termination_forbidden", + ErrorCategory.conflict, + "The upload cannot be terminated in its current state.", + ) + + self.catalog.with_upload_transaction(authorize) + + def record_terminated(self, upload_id: str) -> None: + def terminate(connection: Connection) -> None: + record = self.catalog.get_upload_intent_by_upload_id( + upload_id, + connection=connection, + for_update=True, + ) + if record is None: + return + if record.state == UploadState.processing: + raise ApplicationError( + "upload_termination_forbidden", + ErrorCategory.conflict, + "A processing upload cannot be terminated.", + ) + state = ( + UploadState.ready + if record.state == UploadState.ready + else UploadState.expired + ) + self.catalog.update_upload( + record.intent_id, + state=state, + connection=connection, + clear_upload_id=True, + ) + + self.catalog.with_upload_transaction(terminate) + + def _expire_intent(self, intent_id: str) -> str | None: + def expire(connection: Connection) -> str | None: + record = self.catalog.get_upload_intent( + intent_id, + connection=connection, + for_update=True, + ) + if record is None or record.state not in { + UploadState.pending, + UploadState.accepted, + UploadState.failed, + }: + return None + self.catalog.update_upload( + intent_id, + state=UploadState.expired, + connection=connection, + ) + return record.upload_id + + return self.catalog.with_upload_transaction(expire) + + def _upload_is_active(self, upload_id: str, *, now) -> bool: + cutoff = now.timestamp() - self.settings.upload_intent_ttl_seconds + for suffix in ("", ".info"): + path = self._quarantine_path(f"{upload_id}{suffix}") + try: + if ( + path.is_file() + and not path.is_symlink() + and path.stat().st_mtime > cutoff + ): + return True + except OSError: + continue + return False + + def _read_upload_info(self, upload_id: str) -> dict[str, Any] | None: + path = self._quarantine_path(f"{upload_id}.info") + if not path.is_file() or path.is_symlink(): + return None + try: + with path.open("rb") as handle: + payload = handle.read(65_537) + if len(payload) > 65_536: + return None + result = json.loads(payload) + except (OSError, json.JSONDecodeError): + return None + return result if isinstance(result, dict) else None + + def _quarantine_path(self, name: str) -> Path: + if ( + not name + or len(name) > 260 + or any( + character + not in "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._~-" + for character in name + ) + ): + raise ApplicationError( + "upload_identifier_invalid", + ErrorCategory.validation, + "The upload identifier is invalid.", + ) + root = self.settings.quarantine_root.resolve() + path = (root / name).resolve() + if path.parent != root: + raise RuntimeError("The upload escaped its quarantine root.") + return path + + def _terminate_upload(self, upload_id: str) -> None: + endpoint = self.settings.upload_internal_endpoint + if endpoint is None: + return + request = Request(endpoint + upload_id, method="DELETE") + request.add_header("Tus-Resumable", "1.0.0") + if self.settings.upload_cleanup_token is not None: + request.add_header( + "X-VidXP-Cleanup-Token", + self.settings.upload_cleanup_token.get_secret_value(), + ) + try: + with urlopen(request, timeout=5) as response: + if response.status not in {204, 404}: + raise RuntimeError("tusd rejected upload cleanup.") + except HTTPError as exc: + if exc.code != 404: + raise RuntimeError("tusd upload cleanup failed.") from exc + except URLError as exc: + raise RuntimeError("tusd upload cleanup is unavailable.") from exc + + def _cleanup_upload(self, upload_id: str) -> None: + self._terminate_upload(upload_id) + self.record_terminated(upload_id) + + def _cleanup_after_import(self, upload_id: str) -> None: + try: + self._cleanup_upload(upload_id) + except Exception: + LOGGER.warning( + "Imported upload %s is awaiting quarantine cleanup.", + upload_id, + exc_info=True, + ) + + def _require_configured(self) -> None: + if self.settings.upload_public_endpoint is None: + raise ApplicationError( + "remote_upload_unavailable", + ErrorCategory.unavailable, + "Remote resumable uploads are not configured.", + ) + + @staticmethod + def _require_same_request( + record: UploadIntentRecord, + command: CreateUploadIntentCommand, + ) -> None: + if ( + record.original_filename != command.original_filename + or record.byte_size != command.byte_size + or record.declared_mime_type != command.declared_mime_type + ): + raise ApplicationError( + "idempotency_key_reused", + ErrorCategory.validation, + "The idempotency key was already used for another request.", + ) diff --git a/src/vidxp/workflow_contracts.py b/src/vidxp/workflow_contracts.py new file mode 100644 index 0000000..e49fe75 --- /dev/null +++ b/src/vidxp/workflow_contracts.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import TypeAdapter + +from vidxp.application_models import JobKind, JobQueue, JobRequest + + +APPLICATION_NAME = "vidxp" +WORKFLOW_CLASS_NAME = "VidXPWorkerWorkflows" +WORKFLOW_INSTANCE_NAME = "default" +PROGRESS_EVENT = "vidxp.progress" +ERROR_EVENT = "vidxp.error" + +WORKFLOW_NAMES: dict[JobKind, str] = { + JobKind.media_import: "vidxp.media_import.v1", + JobKind.index: "vidxp.index.v1", + JobKind.search: "vidxp.search.v2", + JobKind.query: "vidxp.query.v1", + JobKind.snippet: "vidxp.snippet.v1", + JobKind.actor_overlay: "vidxp.actor_overlay.v1", + JobKind.prepare_models: "vidxp.prepare_models.v1", +} +WORKFLOW_KINDS = {name: kind for kind, name in WORKFLOW_NAMES.items()} +WORKFLOW_KINDS["vidxp.search.v1"] = JobKind.search +_JOB_REQUEST_ADAPTER = TypeAdapter(JobRequest) + + +def decode_workflow_request(payload: Any) -> JobRequest: + """Validate current jobs while upgrading the retired search-v1 shape.""" + if isinstance(payload, dict) and payload.get("kind") == JobKind.search: + command = payload.get("command") + if ( + isinstance(command, dict) + and "modality" in command + and "modalities" not in command + ): + modality = command["modality"] + command = { + key: value + for key, value in command.items() + if key != "modality" + } + command["modalities"] = (modality,) + payload = {**payload, "command": command} + return _JOB_REQUEST_ADAPTER.validate_python(payload) + +QUEUE_NAMES: dict[JobQueue, str] = { + JobQueue.cpu: "vidxp-cpu", + JobQueue.gpu: "vidxp-gpu", +} +QUEUE_KINDS = {name: queue for queue, name in QUEUE_NAMES.items()} diff --git a/src/vidxp/workflow_runtime.py b/src/vidxp/workflow_runtime.py new file mode 100644 index 0000000..7f2b142 --- /dev/null +++ b/src/vidxp/workflow_runtime.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import hashlib +import json +import sys +from functools import lru_cache +from pathlib import Path + +from pydantic import BaseModel, ConfigDict, Field + +from vidxp import __version__ +from vidxp.capabilities.registry import create_capability_registry +from vidxp.core.manifest import dependency_versions, implementation_digest +from vidxp.settings import ( + ApplicationMode, + LocalExecutionSettings, + VidXPSettings, +) + + +LOCAL_WORKER_BOOTSTRAP_ENV = "VIDXP_LOCAL_WORKER_BOOTSTRAP_FILE" +BUNDLED_POSTGRES_DATABASE_URL = ( + "postgresql+psycopg://vidxp@postgres:5432/vidxp" +) + + +class LocalWorkerBootstrap(BaseModel): + """One-use local worker configuration stored outside process arguments.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + settings: LocalExecutionSettings + database_url: str = Field(min_length=1) + fingerprint: str = Field(pattern=r"^[a-f0-9]{64}$") + + +class LocalWorkerReady(BaseModel): + """Non-secret identity published by a ready local worker.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + pid: int = Field(gt=0) + application_version: str = Field(min_length=1) + fingerprint: str = Field(pattern=r"^[a-f0-9]{64}$") + + +class LocalWorkerStopRequest(BaseModel): + """Repository-local request for the supervised worker to shut down.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + pid: int = Field(gt=0) + application_version: str = Field(min_length=1) + + +def workflow_database_url(settings: VidXPSettings) -> str: + if settings.mode == ApplicationMode.server: + return BUNDLED_POSTGRES_DATABASE_URL + database = settings.layout.workflow_database.resolve() + return f"sqlite:///{database.as_posix()}" + + +@lru_cache(maxsize=1) +def workflow_application_version() -> str: + """Identify the exact workflow implementation, not only the release.""" + + return f"{__version__}+{implementation_digest()[:16]}" + + +@lru_cache(maxsize=16) +def _local_execution_provenance( + external_capabilities: bool, + capability_allowlist: tuple[str, ...], +) -> dict: + registry = create_capability_registry( + external=external_capabilities, + allowlist=capability_allowlist, + ) + return { + "executable": str(Path(sys.executable).resolve()), + "implementation_sha256": implementation_digest(), + "python": sys.version, + "dependencies": dependency_versions(registry), + } + + +def local_worker_bootstrap(settings: VidXPSettings) -> LocalWorkerBootstrap: + local_settings = LocalExecutionSettings.from_settings(settings) + active_database_url = workflow_database_url(settings) + identity = { + "application_version": workflow_application_version(), + "database_url": active_database_url, + "execution": _local_execution_provenance( + settings.external_capabilities, + settings.capability_allowlist, + ), + "settings": local_settings.model_dump(mode="json"), + } + fingerprint = hashlib.sha256( + json.dumps( + identity, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + ).hexdigest() + return LocalWorkerBootstrap( + settings=local_settings, + database_url=active_database_url, + fingerprint=fingerprint, + ) + + +def local_executor_id(settings: VidXPSettings) -> str: + if settings.mode == ApplicationMode.server: + raise ValueError("A local executor ID cannot be used in server mode.") + return f"{workflow_application_version()}-local-0" + + +def server_executor_id(*, role: str) -> str: + if role not in {"cpu", "gpu"}: + raise ValueError("The worker role is invalid.") + return f"{workflow_application_version()}-{role}-0" + + +def sqlite_database_path(database_url: str) -> Path | None: + prefix = "sqlite:///" + if not database_url.startswith(prefix): + return None + return Path(database_url.removeprefix(prefix)) diff --git a/src/vidxp/workflow_worker.py b/src/vidxp/workflow_worker.py new file mode 100644 index 0000000..2c7c4a9 --- /dev/null +++ b/src/vidxp/workflow_worker.py @@ -0,0 +1,256 @@ +from __future__ import annotations + +import argparse +import logging +import os +import signal +from logging.handlers import RotatingFileHandler +from pathlib import Path +from threading import Event +from typing import Callable, Sequence + +from dbos import DBOS, DBOSConfig +from filelock import FileLock + +from vidxp.application_models import JobQueue +from vidxp.composition import create_application +from vidxp.core.manifest import write_json_atomic +from vidxp.infrastructure.dbos_workflows import VidXPWorkerWorkflows +from vidxp.infrastructure.local_files import durable_unlink +from vidxp.settings import VidXPSettings +from vidxp.workflow_contracts import APPLICATION_NAME, QUEUE_NAMES +from vidxp.workflow_runtime import ( + LOCAL_WORKER_BOOTSTRAP_ENV, + LocalWorkerBootstrap, + LocalWorkerReady, + LocalWorkerStopRequest, + local_worker_bootstrap, + server_executor_id, + workflow_application_version, + workflow_database_url, +) + +LOGGER = logging.getLogger(__name__) +_MAXIMUM_LOG_BYTES = 5 * 1024 * 1024 + + +def _arguments(values: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run VidXP DBOS workflows.") + parser.add_argument("--executor-id") + parser.add_argument("--role", choices=("all", "cpu", "gpu"), default="cpu") + parser.add_argument("--lock-file", type=Path) + parser.add_argument("--ready-file", type=Path) + parser.add_argument("--stop-file", type=Path) + parser.add_argument("--log-file", type=Path) + return parser.parse_args(values) + + +def _configure_logging(path: Path | None) -> None: + if path is None: + return + path.parent.mkdir(parents=True, exist_ok=True) + handler = RotatingFileHandler( + path, + maxBytes=_MAXIMUM_LOG_BYTES, + backupCount=2, + encoding="utf-8", + ) + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s %(message)s", + handlers=[handler], + force=True, + ) + + +def _local_bootstrap() -> LocalWorkerBootstrap | None: + raw_path = os.environ.pop(LOCAL_WORKER_BOOTSTRAP_ENV, None) + if raw_path is None: + return None + path = Path(raw_path) + try: + return LocalWorkerBootstrap.model_validate_json( + path.read_text(encoding="utf-8") + ) + finally: + durable_unlink(path, missing_ok=True) + + +def run_worker( + *, + settings: VidXPSettings, + database_url: str, + executor_id: str, + role: str, + ready: Callable[[], None] | None = None, + stop_event: Event | None = None, + stop_file: Path | None = None, +) -> None: + config: DBOSConfig = { + "name": APPLICATION_NAME, + "system_database_url": database_url, + "application_version": workflow_application_version(), + "executor_id": executor_id, + "max_executor_threads": 1 if role == "all" else None, + "use_listen_notify": not database_url.startswith("sqlite:///"), + "dbos_system_schema": ( + None if database_url.startswith("sqlite:///") else "dbos" + ), + } + DBOS(config=config) + try: + VidXPWorkerWorkflows(create_application(settings)) + roles = ("cpu", "gpu") if role == "all" else (role,) + queue_names = [ + QUEUE_NAMES[queue_role] + for queue_role in (JobQueue(value) for value in roles) + ] + DBOS.listen_queues(queue_names) + active_stop_event = stop_event or Event() + DBOS.launch() + for queue_name in queue_names: + DBOS.register_queue( + queue_name, + worker_concurrency=1, + ) + if ready is not None: + ready() + while not active_stop_event.wait(0.25): + if stop_file is None or not stop_file.is_file(): + continue + try: + request = LocalWorkerStopRequest.model_validate_json( + stop_file.read_text(encoding="utf-8") + ) + except (OSError, ValueError): + continue + if ( + request.pid == os.getpid() + and request.application_version + == workflow_application_version() + ): + break + finally: + DBOS.destroy(workflow_completion_timeout_sec=30) + + +def main(arguments: Sequence[str] | None = None) -> None: + options = _arguments(arguments) + _configure_logging(options.log_file) + bootstrap = _local_bootstrap() + settings = ( + bootstrap.settings.application_settings() + if bootstrap is not None + else VidXPSettings() + ) + if bootstrap is not None: + expected_bootstrap = local_worker_bootstrap(settings) + if expected_bootstrap.fingerprint != bootstrap.fingerprint: + raise ValueError( + "The local worker bootstrap identity is invalid." + ) + database_url = ( + bootstrap.database_url + if bootstrap is not None + else workflow_database_url(settings) + ) + role = options.role + executor_id = options.executor_id + if executor_id is None: + if role == "all": + raise ValueError("The all-queue worker requires an executor ID.") + executor_id = server_executor_id(role=role) + + if options.lock_file is None: + stop_event = Event() + previous_handlers = _install_shutdown_handlers(stop_event) + try: + run_worker( + settings=settings, + database_url=database_url, + executor_id=executor_id, + role=role, + stop_event=stop_event, + ) + finally: + _restore_shutdown_handlers(previous_handlers) + return + + lock = FileLock(options.lock_file) + ready_file = options.ready_file + + def mark_ready() -> None: + if ready_file is None: + return + if bootstrap is None: + raise RuntimeError( + "A supervised local worker requires bootstrap identity." + ) + ready_file.parent.mkdir(parents=True, exist_ok=True) + write_json_atomic( + ready_file, + LocalWorkerReady( + pid=os.getpid(), + application_version=workflow_application_version(), + fingerprint=bootstrap.fingerprint, + ).model_dump(mode="json"), + ) + + stop_event = Event() + previous_handlers = _install_shutdown_handlers(stop_event) + try: + with lock: + try: + run_worker( + settings=settings, + database_url=database_url, + executor_id=executor_id, + role=role, + ready=mark_ready, + stop_event=stop_event, + stop_file=options.stop_file, + ) + finally: + if ready_file is not None: + durable_unlink(ready_file, missing_ok=True) + if options.stop_file is not None: + durable_unlink(options.stop_file, missing_ok=True) + finally: + _restore_shutdown_handlers(previous_handlers) + + +def _install_shutdown_handlers( + stop_event: Event, +) -> dict[signal.Signals, signal.Handlers]: + previous_handlers = {} + + def request_shutdown( + _signal_number: int, + _frame: object, + ) -> None: + stop_event.set() + + for signal_name in ("SIGINT", "SIGTERM", "SIGBREAK"): + shutdown_signal = getattr(signal, signal_name, None) + if shutdown_signal is None: + continue + previous_handlers[shutdown_signal] = signal.signal( + shutdown_signal, + request_shutdown, + ) + return previous_handlers + + +def _restore_shutdown_handlers( + previous_handlers: dict[signal.Signals, signal.Handlers], +) -> None: + for shutdown_signal, previous_handler in previous_handlers.items(): + signal.signal(shutdown_signal, previous_handler) + + +if __name__ == "__main__": + try: + main() + except Exception: + LOGGER.exception("The VidXP workflow worker exited unexpectedly.") + raise diff --git a/tests/test_actor_results.py b/tests/test_actor_results.py index d916368..71d9570 100644 --- a/tests/test_actor_results.py +++ b/tests/test_actor_results.py @@ -1,20 +1,26 @@ import unittest -from tempfile import TemporaryDirectory -from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import Mock from vidxp.capabilities.actor.results import ( ActorClusterNotFoundError, + actor_cluster, actor_clusters, actor_detections, - render_actor_result, ) +from vidxp.capabilities.actor.schemas import ActorDetectionsInput from vidxp.core.contracts import IndexConfig +from vidxp.core.cursors import encode_cursor +from vidxp.capabilities.contracts import CapabilityRequestError + + +MEDIA_ID = "123456781234423481234567890abcde" +OTHER_MEDIA_ID = "223456781234423481234567890abcde" +GENERATION_ID = "323456781234423481234567890abcde" class ActorResultTests(unittest.TestCase): def setUp(self): - self.config = IndexConfig.local(video_id="video-1") + self.config = IndexConfig.local(video_id=MEDIA_ID) def test_actor_detection_metadata_is_converted_once(self): storage = Mock() @@ -31,65 +37,171 @@ def test_actor_detection_metadata_is_converted_once(self): "dataset": "local", "split": "local", "run_id": "default", - "video_id": "video-1", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, "modality": "actor", "source_id": "actor:d2", } ] - detections = actor_detections( + page = actor_detections( self.config, "3", storage=storage, + page_size=50, + cursor=None, ) - self.assertEqual(detections[0].bbox, (1, 4, 5, 0)) + self.assertEqual(page.detections[0].bbox, (1, 4, 5, 0)) storage.records.assert_called_once_with( "actor", - video_id="video-1", filters={"cluster_id": "3"}, + limit=51, + offset=0, ) - storage.close.assert_not_called() def test_actor_clusters_summarize_detection_ranges(self): storage = Mock() - storage.records.return_value = [ - {"cluster_id": "1", "timestamp": 4.5}, - {"cluster_id": "1", "timestamp": 1.5}, - {"cluster_id": "2", "timestamp": 9.0}, + legacy_records = [ + { + "cluster_id": "cluster-1", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, + "timestamp": 4.5, + }, + { + "cluster_id": "cluster-1", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, + "timestamp": 1.5, + }, + { + "cluster_id": "cluster-2", + "video_id": OTHER_MEDIA_ID, + "generation_id": GENERATION_ID, + "timestamp": 9.0, + }, ] + storage.records.side_effect = [[], legacy_records] - clusters = actor_clusters(self.config, storage=storage) + page = actor_clusters( + self.config, + storage=storage, + page_size=50, + cursor=None, + ) + clusters = page.clusters self.assertEqual( [cluster.cluster_id for cluster in clusters], - ["1", "2"], + ["cluster-1", "cluster-2"], ) self.assertEqual(clusters[0].detection_count, 2) self.assertEqual(clusters[0].first_timestamp, 1.5) self.assertEqual(clusters[0].last_timestamp, 4.5) + self.assertEqual(clusters[1].media_id, OTHER_MEDIA_ID) + self.assertEqual( + storage.records.call_args_list[0].kwargs, + { + "filters": {"record_kind": "cluster_summary"}, + "limit": 51, + "offset": 0, + }, + ) + self.assertEqual( + storage.records.call_args_list[1].kwargs, + {"limit": 1000, "offset": 0}, + ) - def test_render_actor_result_rejects_an_empty_cluster(self): + def test_actor_cluster_reads_only_the_selected_cluster_once(self): + storage = Mock() + detection_records = [ + { + "cluster_id": "cluster-1", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, + "timestamp": 4.5, + }, + { + "cluster_id": "cluster-1", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, + "timestamp": 1.5, + }, + ] + storage.records.side_effect = [[], detection_records] + + summary = actor_cluster( + self.config, + "cluster-1", + storage=storage, + ) + + self.assertEqual(summary.cluster_id, "cluster-1") + self.assertEqual(summary.detection_count, 2) + self.assertEqual(summary.first_timestamp, 1.5) + self.assertEqual(summary.last_timestamp, 4.5) + self.assertEqual( + storage.records.call_args_list[0].kwargs, + { + "filters": { + "record_kind": "cluster_summary", + "summary_cluster_id": "cluster-1", + }, + "limit": 2, + }, + ) + self.assertEqual( + storage.records.call_args_list[1].kwargs, + {"filters": {"cluster_id": "cluster-1"}}, + ) + + def test_actor_cluster_uses_the_typed_not_found_error(self): storage = Mock() storage.records.return_value = [] with self.assertRaises(ActorClusterNotFoundError): - render_actor_result( + actor_cluster( self.config, "missing", - "input.mp4", - "output.mp4", storage=storage, ) - def test_render_actor_result_returns_output_details(self): + def test_actor_clusters_reject_cross_media_identity_collision(self): storage = Mock() - storage.records.return_value = [ + legacy_records = [ { - "detection_id": "d2", - "cluster_id": "3", - "frame_index": 2, - "timestamp": 0.2, + "cluster_id": "legacy-cluster", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, + "timestamp": 1.0, + }, + { + "cluster_id": "legacy-cluster", + "video_id": OTHER_MEDIA_ID, + "generation_id": GENERATION_ID, + "timestamp": 2.0, + }, + ] + storage.records.side_effect = [[], legacy_records] + + with self.assertRaisesRegex(RuntimeError, "multiple index identities"): + actor_clusters( + self.config, + storage=storage, + page_size=50, + cursor=None, + ) + + def test_actor_detection_cursor_round_trips_through_public_input(self): + cluster_id = "cluster-" + ("a" * 64) + storage = Mock() + records = [ + { + "detection_id": f"detection-{index}", + "cluster_id": cluster_id, + "frame_index": index, + "timestamp": index / 10, "bbox_top": 1, "bbox_right": 4, "bbox_bottom": 5, @@ -97,27 +209,136 @@ def test_render_actor_result_returns_output_details(self): "dataset": "local", "split": "local", "run_id": "default", - "video_id": "video-1", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, "modality": "actor", - "source_id": "actor:d2", + "source_id": f"actor:detection-{index}", } + for index in range(3) + ] + storage.records.side_effect = lambda *args, **options: records[ + options["offset"] : options["offset"] + options["limit"] ] - with TemporaryDirectory() as directory: - output = Path(directory) / "actor.mp4" - with patch( - "vidxp.capabilities.actor.results.render_actor_video" - ) as renderer: - result = render_actor_result( - self.config, - "3", - "input.mp4", - output, - storage=storage, - ) - - renderer.assert_called_once() - self.assertEqual(result.output_path, output) - self.assertEqual(result.detection_count, 1) + + first = actor_detections( + self.config, + cluster_id, + storage=storage, + page_size=2, + cursor=None, + ) + + self.assertIsNotNone(first.next_cursor) + self.assertGreater(len(first.next_cursor or ""), 128) + request = ActorDetectionsInput( + cluster_id=cluster_id, + page_size=2, + cursor=first.next_cursor, + ) + second = actor_detections( + self.config, + request.cluster_id, + storage=storage, + page_size=request.page_size, + cursor=request.cursor, + ) + + self.assertEqual( + [detection.detection_id for detection in second.detections], + ["detection-2"], + ) + self.assertIsNone(second.next_cursor) + self.assertEqual( + [call.kwargs["offset"] for call in storage.records.call_args_list], + [0, 2], + ) + self.assertEqual( + [call.kwargs["limit"] for call in storage.records.call_args_list], + [3, 3], + ) + + def test_actor_detection_cursor_rejects_noncanonical_payloads(self): + cluster_id = "cluster-1" + scope = ( + f"actor:detections:{self.config.fingerprint()}:{cluster_id}" + ) + storage = Mock() + + for position in ( + {"offset": True}, + {"offset": 0, "after": [0, "detection-1"]}, + ): + with self.subTest(position=position): + with self.assertRaises(CapabilityRequestError): + actor_detections( + self.config, + cluster_id, + storage=storage, + page_size=2, + cursor=encode_cursor(scope, position), + ) + storage.records.assert_not_called() + + def test_actor_cluster_pages_use_opaque_cursors(self): + storage = Mock() + records = [ + { + "record_kind": "cluster_summary", + "summary_cluster_id": f"cluster-{index}", + "video_id": MEDIA_ID, + "generation_id": GENERATION_ID, + "detection_count": index + 1, + "first_timestamp": float(index), + "last_timestamp": float(index) + 0.5, + } + for index in range(3) + ] + storage.records.side_effect = lambda *args, **options: records[ + options["offset"] : options["offset"] + options["limit"] + ] + first = actor_clusters( + self.config, + storage=storage, + page_size=2, + cursor=None, + ) + second = actor_clusters( + self.config, + storage=storage, + page_size=2, + cursor=first.next_cursor, + ) + + self.assertIsNone(first.total) + self.assertEqual(len(first.clusters), 2) + self.assertIsNotNone(first.next_cursor) + self.assertEqual( + [cluster.cluster_id for cluster in second.clusters], + ["cluster-2"], + ) + self.assertIsNone(second.next_cursor) + + with self.assertRaises(CapabilityRequestError): + actor_detections( + self.config, + "cluster-2", + storage=storage, + page_size=2, + cursor=first.next_cursor, + ) + + def test_actor_detections_rejects_an_empty_cluster(self): + storage = Mock() + storage.records.return_value = [] + + with self.assertRaises(ActorClusterNotFoundError): + actor_detections( + self.config, + "missing", + storage=storage, + page_size=50, + cursor=None, + ) if __name__ == "__main__": diff --git a/tests/test_application.py b/tests/test_application.py index 5ea2650..f3bb2d4 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,299 +1,1184 @@ +import json import unittest from pathlib import Path from tempfile import TemporaryDirectory -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch -from vidxp.application import VidXPService +from pydantic import ValidationError + +from vidxp.application import VidXPApplication +from vidxp.application_models import ( + ApplicationError, + CapabilityDependencyCheck, + ComponentReadiness, + CreateActorOverlayCommand, + CreateIndexCommand, + DependencyKind, + DependencyCheckCommand, + DependencyCheckResult, + DependencyUnavailableError, + FusedSearchResult, + IndexResult, + IndexSnapshotReference, + ModelUnavailableError, + PrepareModelsCommand, + QueryAnswerMode, + QueryVideoCommand, + RemoveIndexCommand, + SearchCommand, +) +from vidxp.core.media import MediaUnavailableError +from vidxp.core.contracts import IndexConfig, IndexSchemaError +from vidxp.infrastructure.local_index import LocalIndexBackend +from vidxp.model_contracts import ModelArtifactUnavailableError from vidxp.capabilities.contracts import ( CapabilityDefinition, + CapabilityExecutor, + CapabilityPlugin, OperationDefinition, - PreparationContext, ) -from vidxp.capabilities.scene.config import SceneConfig +from vidxp.capabilities.registry import ( + CapabilityRegistry, + create_capability_registry, +) from vidxp.capabilities.schemas import SearchInput, SearchResult -from vidxp.core.contracts import IndexConfig +from vidxp.capabilities.actor.schemas import ( + ActorClusterInput, + ActorClusterSummary, + ActorClustersInput, + ActorClustersOutput, + ActorDetection, + ActorDetectionsInput, + ActorDetectionsOutput, +) +from vidxp.control_plane import ControlPlaneApplication +from vidxp.composition import create_local_application +from vidxp.repository_layout import RepositoryLayout +from vidxp.repositories import RepositoryConfigError +from vidxp.runtime import ModelRuntime +from vidxp.settings import VidXPSettings +from vidxp.ports import IndexStore -class ApplicationServiceTests(unittest.TestCase): - def test_execute_supports_validated_operation_without_an_index(self): - capability = CapabilityDefinition( - name="export", - description="Export results.", - extra="export", +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" +SNAPSHOT_SHA256 = "a" * 64 + + +class ApplicationTests(unittest.TestCase): + def application( + self, + root: str | Path, + *, + registry: CapabilityRegistry | None = None, + backend: Mock | None = None, + ) -> tuple[VidXPApplication, Mock]: + settings = VidXPSettings( + repository_root=Path(root), + runtime_backend="cpu", + ) + runtime = ModelRuntime(settings) + active_backend = backend or Mock() + media_service = Mock() + artifact_service = Mock() + return ( + VidXPApplication( + settings=settings, + layout=RepositoryLayout(root=Path(root)), + registry=registry or create_capability_registry(), + runtime=runtime, + index_backend=active_backend, + media=media_service, + artifacts=artifact_service, + index_status=lambda: active_backend.status(Path(root)), + ), + active_backend, + ) + + def indexed_application( + self, + handler, + manager, + ) -> VidXPApplication: + definition = CapabilityDefinition( + name="indexed", + description="Indexed provider.", + extra="indexed", + collection_name="indexed", + index_stage="indexed", + execution_group="indexed", operations={ - "run": OperationDefinition( + "search": OperationDefinition( input_model=SearchInput, output_model=SearchResult, - handler=lambda context, request: { - "query_id": "export:1", - "query": request.query, - "modality": "export", - "hits": (), - }, - requires_index=False, ) }, ) - service = VidXPService() + + registry = CapabilityRegistry( + ( + CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + indexer=Mock(), + operations={"search": handler}, + ), + ), + ) + ) + application, backend = self.application( + "unused", + registry=registry, + ) + backend.active_config.return_value = IndexConfig.local( + enabled_modalities=("indexed",), + collection_names={"indexed": "indexed"}, + ) + backend.open_store.return_value = manager + return application + + def test_local_composition_maps_typed_repository_errors_safely(self): with ( patch( - "vidxp.application.get_capability", - return_value=capability, - ), - patch.object( - service, - "active_config", - side_effect=AssertionError("index should not be loaded"), + "vidxp.composition.resolve_repository", + side_effect=RepositoryConfigError("secret-path"), ), + self.assertRaises(ApplicationError) as caught, ): - result = service.execute( - "export", - "run", - {"query": "result bundle"}, - ) + create_local_application() - self.assertEqual(result.query, "result bundle") + self.assertEqual(caught.exception.code, "configuration_invalid") + self.assertNotIn( + "secret-path", + json.dumps(caught.exception.to_dict()), + ) - def test_missing_index_has_a_stable_status_contract(self): - service = VidXPService("missing-index") - with patch( - "vidxp.application.read_index_status", - return_value=None, + def test_local_composition_does_not_reclassify_unexpected_value_errors(self): + with ( + patch( + "vidxp.composition.resolve_repository", + side_effect=ValueError("programming error"), + ), + self.assertRaisesRegex(ValueError, "programming error"), ): - status = service.index_status() - - self.assertEqual(status["state"], "missing") - self.assertEqual(status["schema_version"], 1) + create_local_application() - def test_active_config_uses_selected_directory_and_device(self): - service = VidXPService("selected-index", device="cuda") - ready = {"state": "ready"} - stored_config = IndexConfig.local( - video_id="video-1", - storage_directory="selected-index", - ) + def test_local_composition_opens_and_closes_services_lazily(self): + application = Mock() + jobs = Mock() with ( + TemporaryDirectory() as directory, patch( - "vidxp.application.require_ready_index", - return_value=ready, - ) as require, + "vidxp.composition.create_application", + return_value=application, + ) as create_application, patch( - "vidxp.application.local_config_from_status", - return_value=stored_config, - ) as restore, + "vidxp.composition.create_job_service", + return_value=jobs, + ) as create_jobs, ): - config, status = service.active_config() + local = create_local_application(index_directory=directory) + self.assertEqual(local.repository.index_directory, Path(directory)) + create_application.assert_not_called() + create_jobs.assert_not_called() - require.assert_called_once_with(Path("selected-index")) - restore.assert_called_once_with( - ready, - storage_directory=Path("selected-index"), - ) - self.assertEqual(config.device, "cuda") - self.assertIs(status, ready) + self.assertIs(local.application, application) + self.assertIs(local.application, application) + create_application.assert_called_once() + create_jobs.assert_not_called() + local.close() + jobs.close.assert_not_called() + + self.assertIs(local.jobs, jobs) + create_jobs.assert_called_once() + local.close() + jobs.close.assert_called_once() + + def test_missing_index_has_shared_status_model(self): + application, backend = self.application("missing") + backend.status.return_value = None + + status = application.index_status() - def test_search_is_a_thin_adapter_over_the_core(self): - service = VidXPService() - expected = SearchResult( - query_id="scene:1", - query="yellow taxi", - modality="scene", + self.assertEqual(status.state, "missing") + self.assertEqual(status.schema_version, 1) + self.assertIsInstance(application, ControlPlaneApplication) + + def test_actor_cluster_is_a_typed_projection_over_execute(self): + application, _ = self.application("repository") + expected = ActorClusterSummary( + cluster_id="actor-1", + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + detection_count=2, + first_timestamp=1, + last_timestamp=2, ) with patch.object( - service, + application, "execute", return_value=expected, ) as execute: - result = service.search("scene", "yellow taxi", top_k=7) + cluster = application.actor_cluster("actor-1") - self.assertIs(result, expected) + self.assertEqual(cluster, expected) execute.assert_called_once_with( - "scene", - "search", - {"query": "yellow taxi", "top_k": 7}, + "actor", + "cluster", + {"cluster_id": "actor-1"}, ) - def test_execute_translates_missing_optional_dependency(self): - def missing_dependency(_context, _request): - raise ModuleNotFoundError("No module named 'clip'", name="clip") + def test_pinned_search_reopens_the_requested_snapshot(self): + contexts = [] - operation = OperationDefinition( - input_model=SearchInput, - output_model=SearchResult, - handler=missing_dependency, - requires_index=False, + def handler(context, request): + contexts.append(context) + return SearchResult( + query_id="indexed:query", + query=request.query, + modality="indexed", + ) + + manager = MagicMock() + manager.__enter__.return_value = Mock(spec=IndexStore) + application = self.indexed_application(handler, manager) + backend = application.index_backend + pinned = IndexConfig.local( + enabled_modalities=("indexed",), + collection_names={"indexed": "indexed"}, + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, ) - capability = CapabilityDefinition( - name="scene", - description="Search visual scenes.", - extra="scene", - operations={"search": operation}, + backend.config_for_snapshot.return_value = pinned + + result = application.search( + SearchCommand(modalities=("indexed",), query="query"), + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), ) - service = VidXPService() - with patch( - "vidxp.application.get_capability", - return_value=capability, - ): - with self.assertRaisesRegex( - RuntimeError, - r'clip is unavailable.*pip install "vidxp\[scene\]"', - ): - service.execute( - "scene", - "search", - {"query": "yellow taxi"}, - ) - def test_create_index_centralizes_storage_and_runtime_configuration(self): - service = VidXPService("selected-index", device="cuda") - with patch( - "vidxp.application.index_video", - return_value={"scene_frames": 1}, - ) as index: - summary = service.create_index( - "video.mp4", - modalities=("scene",), - frame_stride=5, + self.assertEqual(result.query, "query") + backend.active_config.assert_not_called() + backend.config_for_snapshot.assert_called_once_with( + application.index_directory, + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + device="cpu", + ) + backend.open_store.assert_called_once_with(pinned) + self.assertIs(contexts[0].storage, manager.__enter__.return_value) + + def test_query_reuses_one_pinned_store_and_preserves_media_scope(self): + requests = [] + + def handler(_context, request): + requests.append(request) + return SearchResult( + query_id="indexed:query", + query=request.query, + modality="indexed", ) - self.assertEqual(summary, {"scene_frames": 1}) - config = index.call_args.kwargs["config"] - self.assertEqual(config.storage_directory, "selected-index") - self.assertEqual(config.device, "cuda") + manager = MagicMock() + manager.__enter__.return_value = Mock(spec=IndexStore) + application = self.indexed_application(handler, manager) + pinned = IndexConfig.local( + enabled_modalities=("indexed",), + collection_names={"indexed": "indexed"}, + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ) + application.index_backend.config_for_snapshot.return_value = pinned + + result = application.query_video( + QueryVideoCommand( + question="What happens?", + media_id=MEDIA_ID, + modalities=("indexed",), + ), + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + + self.assertEqual(result.mode, QueryAnswerMode.no_evidence) + self.assertEqual(requests[0].media_id, MEDIA_ID) + application.index_backend.open_store.assert_called_once_with(pinned) + + def test_actor_render_reuses_one_pinned_store_and_context(self): + contexts = [] + calls = [] + + def cluster_handler(context, request): + contexts.append(context) + calls.append(("cluster", request.cluster_id)) + return ActorClusterSummary( + cluster_id=request.cluster_id, + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + detection_count=1, + first_timestamp=1, + last_timestamp=1, + ) + + def detections_handler(context, request): + contexts.append(context) + calls.append(("detections", request.cursor)) + return ActorDetectionsOutput( + cluster_id=request.cluster_id, + detections=( + ActorDetection( + detection_id="detection-1", + cluster_id=request.cluster_id, + frame_index=1, + timestamp=1, + bbox=(0, 0, 10, 10), + dataset="local", + split="local", + run_id="default", + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + modality="actor", + source_id="frame-1", + ), + ), + ) + + definition = CapabilityDefinition( + name="actor", + description="Actor provider.", + extra="actor", + collection_name="actor", + index_stage="actor", + execution_group="actor", + operations={ + "cluster": OperationDefinition( + input_model=ActorClusterInput, + output_model=ActorClusterSummary, + ), + "detections": OperationDefinition( + input_model=ActorDetectionsInput, + output_model=ActorDetectionsOutput, + ), + }, + ) + registry = CapabilityRegistry( + ( + CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + indexer=Mock(), + operations={ + "cluster": cluster_handler, + "detections": detections_handler, + }, + ), + ), + ) + ) + manager = MagicMock() + manager.__enter__.return_value = Mock(spec=IndexStore) + application, backend = self.application( + "repository", + registry=registry, + ) + pinned = IndexConfig.local( + enabled_modalities=("actor",), + collection_names={"actor": "actor"}, + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ) + backend.config_for_snapshot.return_value = pinned + backend.open_store.return_value = manager + application.artifacts.create_actor_overlay.return_value = Mock() + + application.render_actor( + CreateActorOverlayCommand(cluster_id="actor-1"), + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + ) + + backend.active_config.assert_not_called() + backend.open_store.assert_called_once_with(pinned) + self.assertEqual( + calls, + [("cluster", "actor-1"), ("detections", None)], + ) + self.assertEqual(len({id(context) for context in contexts}), 1) + application.artifacts.create_actor_overlay.assert_called_once() + artifact_call = ( + application.artifacts.create_actor_overlay.call_args.kwargs + ) + self.assertEqual(artifact_call["media_id"], MEDIA_ID) + self.assertEqual(artifact_call["generation_id"], GENERATION_ID) + + def test_create_index_builds_one_central_config(self): + with TemporaryDirectory() as directory: + application, backend = self.application(directory) + application.media.require_record.return_value = Mock( + original_filename="video.mp4", + sha256="1" * 64, + ) + application.media.content.return_value = Mock( + path=Path("managed.mp4") + ) + backend.create.return_value = { + "media_id": MEDIA_ID, + "generation_id": GENERATION_ID, + "snapshot_id": SNAPSHOT_ID, + "active_media_count": 1, + "record_counts": {"scene": 1}, + } + + result = application.create_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + frame_stride=5, + scene_sample_fps=2.0, + ) + ) + + self.assertEqual( + result, + IndexResult( + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + snapshot_id=SNAPSHOT_ID, + active_media_count=1, + record_counts={"scene": 1}, + ), + ) + config = backend.create.call_args.kwargs["config"] self.assertEqual(config.enabled_modalities, ("scene",)) self.assertEqual(config.frame_stride, 5) + self.assertEqual(config.options_for("scene")["sample_fps"], 2.0) + self.assertEqual(config.device, "cpu") - def test_dependency_checks_return_a_transport_neutral_contract(self): - service = VidXPService() - with patch( - "vidxp.application.dependency_checks", - return_value=( - {"name": "CLIP", "ok": False, "error": "missing"}, - ), - ): - result = service.check_dependencies(("scene",)) - - self.assertFalse(result["ok"]) - failed = [check for check in result["checks"] if not check["ok"]] - self.assertEqual(failed, [ - {"name": "CLIP", "ok": False, "error": "missing"} - ]) - - def test_model_preparation_reports_progress_without_cli_dependencies(self): - service = VidXPService(device="cuda") - events = [] - prepare = Mock( - side_effect=lambda context, progress: ( - progress( - { - "state": "preparing", - "stage": "scene_model", - "message": "Preparing scene model", + def test_operation_definition_is_metadata_and_executor_owns_handler(self): + definition = CapabilityDefinition( + name="export", + description="Export results.", + extra="export", + operations={ + "run": OperationDefinition( + input_model=SearchInput, + output_model=SearchResult, + requires_index=False, + ) + }, + ) + plugin = CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + operations={ + "run": lambda _context, request: { + "query_id": "export:1", + "query": request.query, + "modality": "export", + "hits": (), } - ), - (SceneConfig.model_validate(context.settings).model,), - )[1] + } + ), ) - capability = Mock( - prepare=prepare, - config_model=SceneConfig, + application, _ = self.application( + "unused", + registry=CapabilityRegistry((plugin,)), ) - with ( - patch( - "vidxp.application.dependency_checks", - return_value=(), + + result = application.execute( + "export", + "run", + {"query": "bundle"}, + ) + + self.assertEqual(result.query, "bundle") + + def test_search_reuses_the_registered_capability_operation(self): + calls = [] + + def handler(context, request): + calls.append((context, request)) + return SearchResult( + query_id="indexed:1", + query=request.query, + modality="indexed", + ) + + manager = MagicMock() + manager.__enter__.return_value = Mock(spec=IndexStore) + application = self.indexed_application(handler, manager) + + result = application.search( + SearchCommand( + modalities=("indexed",), + query="yellow taxi", + top_k=7, + ) + ) + + self.assertIsInstance(result, FusedSearchResult) + self.assertEqual(result.modalities, ("indexed",)) + self.assertEqual(calls[0][1].query, "yellow taxi") + self.assertEqual(calls[0][1].top_k, 7) + self.assertIs( + calls[0][0].storage, + manager.__enter__.return_value, + ) + + def test_default_search_filters_indexed_non_search_capabilities(self): + searched: list[str] = [] + + def search_plugin(name: str) -> CapabilityPlugin: + definition = CapabilityDefinition( + name=name, + description=f"{name} search.", + extra=name, + collection_name=name, + index_stage=name, + execution_group=name, + operations={ + "search": OperationDefinition( + input_model=SearchInput, + output_model=SearchResult, + ) + }, + ) + + def handler(_context, request): + searched.append(name) + return SearchResult( + query_id=f"{name}:query", + query=request.query, + modality=name, + ) + + return CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + indexer=Mock(), + operations={"search": handler}, + ), + ) + + actor = CapabilityPlugin( + definition=CapabilityDefinition( + name="actor", + description="Actor clusters.", + extra="actor", + collection_name="actor", + index_stage="actor", + execution_group="actor", + operations={ + "clusters": OperationDefinition( + input_model=ActorClustersInput, + output_model=ActorClustersOutput, + ) + }, ), - patch( - "vidxp.application.get_capability", - return_value=capability, + executor_factory=lambda: CapabilityExecutor( + indexer=Mock(), + operations={ + "clusters": Mock( + side_effect=AssertionError( + "Search must not execute actor clusters." + ) + ) + }, ), - ): - result = service.prepare_models( - ("scene",), - progress_callback=events.append, + ) + registry = CapabilityRegistry( + ( + search_plugin("scene"), + search_plugin("dialogue"), + actor, + ) + ) + manager = MagicMock() + manager.__enter__.return_value = Mock(spec=IndexStore) + application, backend = self.application( + "repository", + registry=registry, + ) + backend.active_config.return_value = IndexConfig.local( + enabled_modalities=("scene", "dialogue", "actor"), + collection_names={ + "scene": "scene", + "dialogue": "dialogue", + "actor": "actor", + }, + ) + backend.open_store.return_value = manager + + result = application.search(SearchCommand(query="taxi")) + + self.assertEqual(searched, ["scene", "dialogue"]) + self.assertEqual(result.modalities, ("scene", "dialogue")) + + def test_application_boundary_returns_stable_validation_error(self): + application, _ = self.application("unused") + + with self.assertRaises(ApplicationError) as raised: + application.execute("scene", "unknown", {}) + + self.assertEqual(raised.exception.code, "invalid_request") + self.assertEqual( + raised.exception.to_dict()["category"], + "validation", + ) + + def test_validation_error_does_not_expose_input(self): + definition = CapabilityDefinition( + name="validate", + description="Validate input.", + extra="validate", + operations={ + "run": OperationDefinition( + input_model=SearchInput, + output_model=SearchResult, + requires_index=False, + ) + }, + ) + application, _ = self.application( + "unused", + registry=CapabilityRegistry( + ( + CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + operations={ + "run": lambda _context, request: { + "query_id": "validate:1", + "query": request.query, + "modality": "validate", + } + } + ), + ), + ) + ), + ) + + with self.assertRaises(ApplicationError) as raised: + application.execute( + "validate", + "run", + { + "query": "hello", + "secret": "do-not-leak", + }, + ) + + self.assertEqual(raised.exception.code, "invalid_request") + self.assertNotIn( + "do-not-leak", + json.dumps(raised.exception.to_dict()), + ) + + def test_missing_media_error_does_not_expose_path(self): + application, _ = self.application("unused") + secret_path = Path("private/customer/video.mp4") + application.media.require_record.side_effect = ( + MediaUnavailableError("secret") + ) + + with self.assertRaises(ApplicationError) as raised: + application.create_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) ) - prepare.assert_called_once() - context = prepare.call_args.args[0] - self.assertIsInstance(context, PreparationContext) - self.assertEqual(context.device, "cuda") - self.assertEqual(result["device"], "cuda") - self.assertEqual(events[0]["stage"], "scene_model") + self.assertEqual(raised.exception.code, "resource_not_found") + self.assertNotIn( + str(secret_path), + json.dumps(raised.exception.to_dict()), + ) - def test_clear_removes_only_known_run_state_after_clearing_collections(self): + def test_downstream_missing_file_is_not_misclassified_as_media(self): with TemporaryDirectory() as directory: - index_directory = Path(directory) / "index" - index_directory.mkdir() - for name in ( - "index_status.json", - "manifest.json", - "timings.jsonl", - "failures.jsonl", - "run.complete.json", - ): - (index_directory / name).write_text("{}", encoding="utf-8") - unrelated = index_directory / "keep.txt" - unrelated.write_text("keep", encoding="utf-8") - checkpoint_directory = index_directory / "checkpoints" - checkpoint_directory.mkdir() - (checkpoint_directory / "one.json").write_text( - "{}", - encoding="utf-8", + path = Path(directory) / "video.mp4" + path.write_bytes(b"video") + application, backend = self.application(directory) + application.media.require_record.return_value = Mock( + original_filename="video.mp4", + sha256="1" * 64, ) + application.media.content.return_value = Mock(path=path) + backend.create.side_effect = FileNotFoundError("ffmpeg") - storage = Mock() - storage.__enter__ = Mock(return_value=storage) - storage.__exit__ = Mock(return_value=None) - service = VidXPService(index_directory) - with ( - patch( - "vidxp.application.indexing_in_progress", - return_value=False, + with self.assertRaises(DependencyUnavailableError) as raised: + application.create_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + ) + + self.assertEqual(raised.exception.code, "dependency_unavailable") + self.assertNotEqual(raised.exception.category, "not_found") + + def test_open_store_dependency_failure_is_stable(self): + application, backend = self.application("unused") + backend.active_config.return_value = IndexConfig.local( + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + ) + backend.open_store.side_effect = ModuleNotFoundError("chromadb") + + with self.assertRaises(DependencyUnavailableError) as raised: + application.execute( + "scene", + "search", + {"query": "hello"}, + ) + + payload = json.dumps(raised.exception.to_dict()) + self.assertNotIn("chromadb", payload) + self.assertIsInstance( + raised.exception.__cause__, + ModuleNotFoundError, + ) + + def test_prepare_dependency_failure_is_stable(self): + definition = CapabilityDefinition( + name="prepare-only", + description="Prepare a provider.", + extra="prepare-only", + operations={ + "noop": OperationDefinition( + input_model=SearchInput, + output_model=SearchResult, + requires_index=False, + ) + }, + prepares_models=True, + ) + plugin = CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + operations={ + "noop": lambda _context, request: { + "query_id": "noop:1", + "query": request.query, + "modality": "noop", + } + }, + prepare=Mock( + side_effect=ModuleNotFoundError("provider.internal") ), - patch( - "vidxp.application.read_index_status", - return_value=None, + ), + ) + registry = CapabilityRegistry((plugin,)) + registry.dependency_checks = Mock(return_value=()) + application, _ = self.application("unused", registry=registry) + + with self.assertRaises(DependencyUnavailableError) as raised: + application.prepare_models( + PrepareModelsCommand(modalities=("prepare-only",)) + ) + + self.assertNotIn( + "provider.internal", + json.dumps(raised.exception.to_dict()), + ) + + def test_missing_model_is_not_misclassified_as_a_package_dependency(self): + application, backend = self.application("unused") + application.media.require_record.return_value = Mock( + original_filename="video.mp4", + sha256="1" * 64, + ) + application.media.content.return_value = Mock( + path=Path("video.mp4") + ) + backend.create.side_effect = ModelArtifactUnavailableError("scene") + + with self.assertRaises(ModelUnavailableError) as raised: + application.create_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + ) + + self.assertEqual(raised.exception.code, "model_unavailable") + self.assertNotIn( + "pip install", + json.dumps(raised.exception.to_dict()), + ) + + def test_unexpected_handler_error_is_not_misclassified(self): + failure = RuntimeError("implementation bug") + definition = CapabilityDefinition( + name="broken", + description="Broken provider.", + extra="broken", + operations={ + "run": OperationDefinition( + input_model=SearchInput, + output_model=SearchResult, + requires_index=False, + ) + }, + ) + plugin = CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + operations={ + "run": Mock(side_effect=failure), + } + ), + ) + application, _ = self.application( + "unused", + registry=CapabilityRegistry((plugin,)), + ) + + with self.assertRaises(RuntimeError) as raised: + application.execute("broken", "run", {"query": "hello"}) + + self.assertIs(raised.exception, failure) + + def test_indexed_operation_uses_and_closes_exact_injected_store(self): + store = Mock(spec=IndexStore) + manager = MagicMock() + manager.__enter__.return_value = store + seen = [] + + def handle(context, request): + seen.append(context.require_storage()) + return { + "query_id": "indexed:1", + "query": request.query, + "modality": "indexed", + } + + application = self.indexed_application(handle, manager) + + result = application.execute( + "indexed", + "search", + {"query": "hello"}, + ) + + self.assertEqual(result.query_id, "indexed:1") + self.assertEqual(seen, [store]) + manager.__enter__.assert_called_once_with() + manager.__exit__.assert_called_once_with(None, None, None) + + def test_store_closes_when_output_validation_fails(self): + store = Mock(spec=IndexStore) + manager = MagicMock() + manager.__enter__.return_value = store + application = self.indexed_application( + lambda _context, _request: {"query": "missing fields"}, + manager, + ) + + with self.assertRaises(ApplicationError) as raised: + application.execute( + "indexed", + "search", + {"query": "hello"}, + ) + + self.assertEqual(raised.exception.code, "invalid_request") + exit_args = manager.__exit__.call_args.args + self.assertIs(exit_args[0], ValidationError) + + def test_store_closes_when_handler_fails(self): + store = Mock(spec=IndexStore) + manager = MagicMock() + manager.__enter__.return_value = store + failure = RuntimeError("implementation bug") + application = self.indexed_application( + Mock(side_effect=failure), + manager, + ) + + with self.assertRaises(RuntimeError) as raised: + application.execute( + "indexed", + "search", + {"query": "hello"}, + ) + + self.assertIs(raised.exception, failure) + exit_args = manager.__exit__.call_args.args + self.assertIs(exit_args[0], RuntimeError) + self.assertIs(exit_args[1], failure) + + def test_dependency_check_returns_shared_result_model(self): + registry = create_capability_registry() + registry.dependency_checks = Mock( + return_value=( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.distribution, + name="transformers", + requirement="transformers>=5,<6", + ok=False, + error="distribution is not installed", ), - patch( - "vidxp.application.IndexStorage", - return_value=storage, + ) + ) + application, _ = self.application("unused", registry=registry) + + result = application.check_dependencies( + DependencyCheckCommand(modalities=("scene",)) + ) + + self.assertFalse(result.ok) + self.assertEqual(result.modalities, ("scene",)) + + @patch("vidxp.application.which", return_value=None) + def test_dependency_check_reports_missing_media_executables(self, _which): + registry = create_capability_registry() + registry.dependency_checks = Mock(return_value=()) + application, _ = self.application("unused", registry=registry) + + result = application.check_dependencies( + DependencyCheckCommand(modalities=("scene",)) + ) + + self.assertFalse(result.ok) + self.assertEqual( + [(check.capability, check.name) for check in result.checks], + [("media", "ffmpeg"), ("media", "ffprobe")], + ) + self.assertTrue( + all( + "does not install OS packages" in (check.error or "") + for check in result.checks + ) + ) + + def test_runtime_readiness_includes_dependency_failures(self): + application, _ = self.application("unused") + application.control_plane_readiness = Mock( + return_value=( + ComponentReadiness( + name="catalog", + ready=True, + message="available", ), - ): - cleared = service.clear_index() + ) + ) + application._index_storage_readiness = Mock( + return_value=ComponentReadiness( + name="index_storage", + ready=True, + message="available", + ) + ) + application.check_dependencies = Mock( + return_value=DependencyCheckResult( + ok=False, + modalities=("scene",), + checks=(), + ) + ) + + readiness = application.runtime_readiness() + + self.assertFalse(readiness.ready) + self.assertFalse(readiness.dependencies.ok) + + def test_prepare_uses_injected_runtime_and_executor(self): + definition = CapabilityDefinition( + name="prepare-only", + description="Prepare a provider.", + extra="prepare-only", + operations={ + "noop": OperationDefinition( + input_model=SearchInput, + output_model=SearchResult, + requires_index=False, + ) + }, + prepares_models=True, + ) + prepare = Mock(return_value=("model",)) + plugin = CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + operations={ + "noop": lambda _context, request: { + "query_id": "noop:1", + "query": request.query, + "modality": "noop", + "hits": (), + } + }, + prepare=prepare, + ), + ) + registry = CapabilityRegistry((plugin,)) + registry.dependency_checks = Mock(return_value=()) + application, _ = self.application("unused", registry=registry) + + result = application.prepare_models( + PrepareModelsCommand(modalities=("prepare-only",)) + ) + + self.assertEqual(result.prepared, ("model",)) + self.assertIs( + prepare.call_args.args[0].runtime, + application.runtime, + ) + + def test_clear_delegates_all_persistence_cleanup_to_backend(self): + with TemporaryDirectory() as directory: + application, backend = self.application(directory) + index = application.index_directory + index.mkdir(parents=True) + (index / "manifest.json").write_text("{}", encoding="utf-8") + unrelated = index / "keep.txt" + unrelated.write_text("keep", encoding="utf-8") + backend.indexing_in_progress.return_value = False + backend.status.return_value = None + + self.assertTrue(application.clear_index()) - self.assertTrue(cleared) - storage.clear.assert_called_once_with() - self.assertTrue(unrelated.is_file()) - self.assertFalse( - (index_directory / "index_status.json").exists() + backend.clear.assert_called_once() + self.assertTrue((index / "manifest.json").exists()) + self.assertTrue(unrelated.exists()) + + def test_remove_delegates_to_index_backend(self): + application, backend = self.application("repository") + backend.remove.return_value = True + + self.assertTrue( + application.remove_from_index( + RemoveIndexCommand(media_id=MEDIA_ID) ) - self.assertFalse(checkpoint_directory.exists()) + ) + + config, media_id = backend.remove.call_args.args + self.assertEqual( + Path(config.storage_directory), + Path("repository") / "indexes", + ) + self.assertEqual(media_id, MEDIA_ID) - def test_clear_translates_missing_storage_dependency(self): + def test_generation_storage_validation_checks_identity_and_count(self): + config = IndexConfig.local( + video_id="episode-1", + generation_id="generation-1", + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + ) + storage = Mock() + storage.records.return_value = [ + { + "generation_id": "generation-1", + "video_id": "episode-1", + "modality": "scene", + } + ] + LocalIndexBackend._validate_generation_records( + storage, + config, + {"scene": 1}, + ) + + with self.assertRaisesRegex( + IndexSchemaError, + "record count", + ): + LocalIndexBackend._validate_generation_records( + storage, + config, + {"scene": 2}, + ) + + def test_local_backend_injects_storage_for_generation_build(self): with TemporaryDirectory() as directory: - service = VidXPService(directory) + settings = VidXPSettings( + repository_root=directory, + runtime_backend="cpu", + ) + registry = create_capability_registry() + backend = LocalIndexBackend( + registry, + ModelRuntime(settings), + settings.layout, + ) + cleanup_storage = MagicMock() + storage = MagicMock() + storage.__enter__.return_value = storage + config = IndexConfig.local( + video_id=MEDIA_ID, + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=settings.layout.indexes, + ) + snapshot = Mock( + snapshot_id="a" * 32, + generations={"media": Mock()}, + ) with ( patch( - "vidxp.application.indexing_in_progress", - return_value=False, + "vidxp.infrastructure.local_index.IndexStorage", + side_effect=(cleanup_storage, storage), ), patch( - "vidxp.application.read_index_status", - return_value=None, + "vidxp.infrastructure.local_index.index_video", + return_value={"scene_frames": 1}, + ) as index_video, + patch( + "vidxp.infrastructure.local_index.LocalSnapshotRepository." + "generation_reference", + return_value=Mock(), ), patch( - "vidxp.application.IndexStorage", - side_effect=ModuleNotFoundError( - "No module named 'chromadb'", - name="chromadb", - ), + "vidxp.infrastructure.local_index.LocalSnapshotRepository." + "publish_generation", + return_value=snapshot, + ), + patch.object( + LocalIndexBackend, + "_validate_generation_records", + return_value={"scene": 1}, + ), + patch( + "vidxp.infrastructure.local_index.ManifestStore." + "record_storage_counts", ), ): - with self.assertRaisesRegex( - RuntimeError, - r'chromadb is unavailable.*pip install "vidxp\[storage\]"', - ): - service.clear_index() + result = backend.create( + Path("video.mp4"), + config=config, + progress=None, + cancellation=None, + source_name=None, + source_checksum="1" * 64, + ) + + self.assertEqual(result["record_counts"], {"scene": 1}) + self.assertEqual(result["media_id"], MEDIA_ID) + self.assertEqual(result["snapshot_id"], "a" * 32) + self.assertIs(index_video.call_args.kwargs["storage"], storage) + self.assertIs( + index_video.call_args.kwargs["manifest_store"].runtime, + backend.runtime, + ) + self.assertIsNotNone( + index_video.call_args.kwargs["config"].generation_id + ) + cleanup_storage.__exit__.assert_called_once() + storage.__exit__.assert_called_once() if __name__ == "__main__": diff --git a/tests/test_application_boundary.py b/tests/test_application_boundary.py new file mode 100644 index 0000000..21deedb --- /dev/null +++ b/tests/test_application_boundary.py @@ -0,0 +1,43 @@ +import unittest + +from vidxp.application_boundary import application_boundary +from vidxp.application_models import ApplicationError +from vidxp.core.artifacts import ArtifactRendererUnavailableError +from vidxp.core.media import MediaProbeUnavailableError + + +class ApplicationBoundaryTests(unittest.TestCase): + def test_missing_media_probe_carries_init_remediation(self): + @application_boundary + def operation(): + raise MediaProbeUnavailableError("missing") + + with self.assertRaises(ApplicationError) as raised: + operation() + + self.assertEqual(raised.exception.code, "media_probe_unavailable") + self.assertEqual( + raised.exception.detail.details["remediation"], + "vidxp init", + ) + + def test_missing_artifact_renderer_carries_init_remediation(self): + @application_boundary + def operation(): + raise ArtifactRendererUnavailableError("missing") + + with self.assertRaises(ApplicationError) as raised: + operation() + + self.assertEqual( + raised.exception.code, + "artifact_renderer_unavailable", + ) + self.assertEqual( + raised.exception.detail.details["remediation"], + "vidxp init", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_authentication.py b/tests/test_authentication.py new file mode 100644 index 0000000..401cbca --- /dev/null +++ b/tests/test_authentication.py @@ -0,0 +1,339 @@ +import unittest +from datetime import datetime, timedelta, timezone +from unittest.mock import Mock, patch + +import jwt +from cryptography.hazmat.primitives.asymmetric import rsa +from pydantic import ValidationError + +from vidxp.application_models import ApplicationError +from vidxp.authentication import ( + OIDCBearerAuthenticator, + StaticBearerAuthenticator, +) +from vidxp.settings import ApplicationMode, VidXPSettings + + +class AuthenticationTests(unittest.TestCase): + def test_static_bearer_uses_one_typed_principal(self): + authenticator = StaticBearerAuthenticator("a" * 32) + + principal = authenticator.authenticate("a" * 32) + + self.assertEqual(principal.subject, "static") + self.assertIn("*", principal.scopes) + with self.assertRaises(ApplicationError) as caught: + authenticator.authenticate("b" * 32) + self.assertEqual(caught.exception.code, "authentication_required") + + def test_oidc_uses_cached_jwks_and_fixed_validation_contract(self): + decoder = Mock( + return_value={ + "sub": "user-1", + "client_id": "client-1", + "scope": "vidxp.read vidxp.write", + } + ) + signing_key = Mock(key="public-key") + jwks = Mock() + jwks.get_signing_key_from_jwt.return_value = signing_key + fake_jwt = Mock() + fake_jwt.PyJWTError = Exception + fake_jwt.PyJWKClient.return_value = jwks + fake_jwt.decode = decoder + + with patch.dict("sys.modules", {"jwt": fake_jwt}): + authenticator = OIDCBearerAuthenticator( + issuer="https://issuer.example/", + audience="https://api.example", + jwks_url="https://issuer.example/jwks", + algorithms=("RS256",), + required_scopes=("vidxp.read",), + ) + principal = authenticator.authenticate("signed-token") + + self.assertEqual(principal.subject, "user-1") + self.assertEqual(principal.client_id, "client-1") + fake_jwt.PyJWKClient.assert_called_once_with( + "https://issuer.example/jwks", + cache_keys=True, + cache_jwk_set=True, + lifespan=300, + timeout=5, + ) + self.assertEqual(decoder.call_args.kwargs["algorithms"], ["RS256"]) + self.assertEqual( + decoder.call_args.kwargs["issuer"], + "https://issuer.example/", + ) + self.assertEqual( + decoder.call_args.kwargs["audience"], + "https://api.example", + ) + self.assertEqual( + decoder.call_args.kwargs["options"]["require"], + ["exp", "iss", "aud", "sub"], + ) + + def test_oidc_preserves_jwk_algorithm_binding(self): + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + public_jwk = jwt.PyJWK.from_dict( + { + **jwt.algorithms.RSAAlgorithm.to_jwk( + private_key.public_key(), + as_dict=True, + ), + "kid": "test-key", + "alg": "RS256", + "use": "sig", + } + ) + now = datetime.now(timezone.utc) + token = jwt.encode( + { + "sub": "user-1", + "iss": "https://issuer.example", + "aud": "https://api.example", + "scope": "vidxp.read", + "iat": now, + "exp": now + timedelta(minutes=5), + }, + private_key, + algorithm="RS512", + headers={"kid": "test-key"}, + ) + authenticator = OIDCBearerAuthenticator( + issuer="https://issuer.example", + audience="https://api.example", + jwks_url="https://issuer.example/jwks", + algorithms=("RS256", "RS512"), + required_scopes=("vidxp.read",), + ) + authenticator._jwks = Mock() + authenticator._jwks.get_signing_key_from_jwt.return_value = public_jwk + + with self.assertRaises(ApplicationError) as caught: + authenticator.authenticate(token) + + self.assertEqual(caught.exception.code, "authentication_required") + + def test_oidc_rejects_missing_configured_scope(self): + signing_key = Mock(key="public-key") + jwks = Mock() + jwks.get_signing_key_from_jwt.return_value = signing_key + fake_jwt = Mock() + fake_jwt.PyJWTError = Exception + fake_jwt.PyJWKClient.return_value = jwks + fake_jwt.decode.return_value = {"sub": "user-1", "scope": "other"} + + with ( + patch.dict("sys.modules", {"jwt": fake_jwt}), + self.assertRaises(ApplicationError) as caught, + ): + OIDCBearerAuthenticator( + issuer="https://issuer.example", + audience="https://api.example", + jwks_url="https://issuer.example/jwks", + algorithms=("RS256",), + required_scopes=("vidxp.read",), + ).authenticate("signed-token") + + self.assertEqual(caught.exception.code, "insufficient_scope") + + def test_oidc_readiness_checks_cached_signing_keys(self): + key_set = Mock( + keys=[ + Mock( + public_key_use="sig", + algorithm_name="RS256", + _jwk_data={"key_ops": ["verify"]}, + ) + ] + ) + jwks = Mock() + jwks.get_jwk_set.return_value = key_set + fake_jwt = Mock() + fake_jwt.PyJWTError = Exception + fake_jwt.PyJWKClient.return_value = jwks + + with patch.dict("sys.modules", {"jwt": fake_jwt}): + authenticator = OIDCBearerAuthenticator( + issuer="https://issuer.example", + audience="https://api.example", + jwks_url="https://issuer.example/jwks", + algorithms=("RS256",), + required_scopes=(), + ) + ready = authenticator.readiness() + jwks.get_jwk_set.return_value = Mock( + keys=[ + Mock( + public_key_use="enc", + algorithm_name="RSA-OAEP", + _jwk_data={"key_ops": ["decrypt"]}, + ) + ] + ) + unusable = authenticator.readiness() + jwks.get_jwk_set.side_effect = OSError("private-network-detail") + unavailable = authenticator.readiness() + + self.assertTrue(ready.ready) + self.assertFalse(unusable.ready) + self.assertFalse(unavailable.ready) + self.assertNotIn("private-network-detail", unavailable.message) + + def test_server_mode_refuses_unauthenticated_http(self): + settings = VidXPSettings( + mode=ApplicationMode.server, + runtime_backend="cpu", + ) + + with self.assertRaisesRegex(ValueError, "requires static bearer or OIDC"): + settings.validate_http_server() + + def test_oidc_settings_require_an_explicit_scope(self): + with self.assertRaisesRegex(ValueError, "at least one scope"): + VidXPSettings( + runtime_backend="cpu", + http_auth_mode="oidc", + http_oidc_issuer="https://issuer.example", + http_oidc_audience="https://api.example", + http_oidc_jwks_url="https://issuer.example/jwks", + ) + + def test_oidc_settings_preserve_exact_issuer(self): + settings = VidXPSettings( + runtime_backend="cpu", + http_auth_mode="oidc", + http_oidc_issuer="https://issuer.example", + http_oidc_audience="https://api.example", + http_oidc_jwks_url="https://issuer.example/jwks", + http_required_scopes=("vidxp.read",), + ) + + self.assertEqual( + settings.http_oidc_issuer, + "https://issuer.example", + ) + + def test_oidc_settings_preserve_uppercase_scheme_for_exact_issuer(self): + settings = VidXPSettings( + runtime_backend="cpu", + http_auth_mode="oidc", + http_oidc_issuer="HTTPS://issuer.example", + http_oidc_audience="https://api.example", + http_oidc_jwks_url="https://issuer.example/jwks", + http_required_scopes=("vidxp.read",), + ) + + self.assertEqual( + settings.http_oidc_issuer, + "HTTPS://issuer.example", + ) + + def test_oidc_http_server_requires_canonical_mcp_resource(self): + settings = VidXPSettings( + runtime_backend="cpu", + http_auth_mode="oidc", + http_oidc_issuer="https://issuer.example", + http_oidc_audience="https://api.example", + http_oidc_jwks_url="https://issuer.example/jwks", + http_required_scopes=("vidxp.read",), + ) + + with self.assertRaisesRegex(ValueError, "mcp_public_url"): + settings.validate_http_server() + + def test_mcp_public_resource_and_host_policy_are_strict(self): + with self.assertRaisesRegex(ValidationError, "ending in /mcp"): + VidXPSettings(mcp_public_url="https://api.example/not-mcp") + with self.assertRaisesRegex( + ValidationError, + "host wildcards", + ): + VidXPSettings(mcp_allowed_hosts=("*.example.com",)) + for origin in ( + "null", + "file://local", + "https://user@example.com", + "https://client.example/path", + "https://client.example?query", + ): + with self.subTest(origin=origin), self.assertRaisesRegex( + ValidationError, + "serialized HTTP origins", + ): + VidXPSettings(mcp_allowed_origins=(origin,)) + + settings = VidXPSettings( + mcp_public_url="https://api.example/mcp/", + mcp_allowed_hosts=("API.EXAMPLE.COM",), + mcp_allowed_origins=("http://localhost:*",), + ) + self.assertEqual( + settings.mcp_public_url, + "https://api.example/mcp", + ) + self.assertEqual( + settings.mcp_allowed_hosts, + ("api.example.com",), + ) + self.assertEqual( + settings.mcp_allowed_origins, + ("http://localhost:*",), + ) + + disabled = VidXPSettings(mcp_allowed_hosts=()) + with self.assertRaisesRegex(ValueError, "allowed MCP host"): + disabled.validate_http_server() + + def test_oidc_rejects_empty_query_and_fragment_delimiters(self): + for field, value in ( + ("http_oidc_issuer", "https://issuer.example?"), + ("http_oidc_issuer", "https://issuer.example#"), + ("http_oidc_jwks_url", "https://issuer.example/jwks#"), + ): + values = { + "runtime_backend": "cpu", + "http_auth_mode": "oidc", + "http_oidc_issuer": "https://issuer.example", + "http_oidc_audience": "https://api.example", + "http_oidc_jwks_url": "https://issuer.example/jwks", + "http_required_scopes": ("vidxp.read",), + field: value, + } + with self.subTest(field=field, value=value): + with self.assertRaises(ValueError): + VidXPSettings(**values) + + def test_oidc_rejects_cleartext_non_loopback_metadata(self): + with self.assertRaisesRegex(ValueError, "must use HTTPS"): + VidXPSettings( + runtime_backend="cpu", + http_auth_mode="oidc", + http_oidc_issuer="http://issuer.example", + http_oidc_audience="https://api.example", + http_oidc_jwks_url="http://issuer.example/jwks", + http_required_scopes=("vidxp.read",), + ) + + def test_oidc_rejects_url_parser_disagreement_characters(self): + with self.assertRaisesRegex(ValueError, "unsafe URL character"): + VidXPSettings( + runtime_backend="cpu", + http_auth_mode="oidc", + http_oidc_issuer="http://localhost", + http_oidc_audience="https://api.example", + http_oidc_jwks_url=( + "http://localhost\\@evil.example/jwks" + ), + http_required_scopes=("vidxp.read",), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_benchmark_cli.py b/tests/test_benchmark_cli.py index 21abef4..4b0b7e6 100644 --- a/tests/test_benchmark_cli.py +++ b/tests/test_benchmark_cli.py @@ -1,18 +1,50 @@ import json +import os +import subprocess +import sys import unittest from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import Mock, patch +from packaging.requirements import Requirement +import typer from typer.testing import CliRunner from vidxp import cli +from vidxp.benchmarks import cli as benchmark_cli +from vidxp.composition import LocalApplicationContext +from vidxp.repositories import RepositoryConfig, RepositoryRegistry class BenchmarkCliTests(unittest.TestCase): - def test_benchmark_app_is_not_loaded_without_its_extra(self): - with patch.object(cli, "requirements_available", return_value=False): - self.assertIsNone(cli._load_benchmark_app()) + def test_benchmark_group_is_available_without_srt_parser(self): + source = Path(__file__).resolve().parents[1] / "src" + code = """ +import importlib.abc +import sys + +class RejectSrt(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == "srt" or fullname.startswith("srt."): + raise ModuleNotFoundError("blocked optional srt dependency") + return None + +sys.meta_path.insert(0, RejectSrt()) +from vidxp import cli +assert cli.benchmark_app is not None +""" + environment = dict(os.environ) + environment["PYTHONPATH"] = str(source) + completed = subprocess.run( + [sys.executable, "-c", code], + env=environment, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) def test_benchmark_uses_global_device_and_json_output(self): runner = CliRunner() @@ -25,14 +57,31 @@ def test_benchmark_uses_global_device_and_json_output(self): annotations = root / "annotations.json" evaluator = root / "evaluator.py" media = root / "media" + replacement = root / "replacement.webm" + overrides = root / "media-overrides.json" annotations.write_text("[]", encoding="utf-8") evaluator.write_text("", encoding="utf-8") media.mkdir() + replacement.write_bytes(b"replacement") + overrides.write_text( + json.dumps({"broken.mp4": replacement.name}), + encoding="utf-8", + ) with ( patch.object( cli, - "VidXPService", - return_value=service, + "create_local_application", + return_value=LocalApplicationContext( + application=service, + jobs=Mock(), + repositories=RepositoryRegistry(config), + repository=RepositoryConfig( + "default", + Path("chroma_data"), + device="cuda", + configured=False, + ), + ), ), patch( "vidxp.benchmarks.cli.run_didemo", @@ -58,12 +107,180 @@ def test_benchmark_uses_global_device_and_json_output(self): str(media), "--run-id", "run-1", + "--media-overrides", + str(overrides), + "--scene-sample-fps", + "2", ], ) self.assertEqual(response.exit_code, 0, response.output) self.assertEqual(json.loads(response.stdout)["rank_at_1"], 0.5) self.assertEqual(run.call_args.kwargs["device"], "cuda") + self.assertEqual(run.call_args.kwargs["scene_sample_fps"], 2.0) + self.assertEqual( + run.call_args.kwargs["media_overrides"], + {"broken.mp4": replacement.resolve()}, + ) + + def test_missing_adapter_dependencies_fail_with_exact_extra_hint(self): + registry = Mock() + registry.requirements_for.return_value = ( + Requirement("missing-runtime>=1"), + ) + failure = { + "name": "missing-runtime", + "requirement": "missing-runtime>=1", + "installed_version": None, + "ok": False, + "error": "distribution is not installed", + } + with ( + patch.object( + benchmark_cli, + "create_capability_registry", + return_value=registry, + ), + patch.object( + benchmark_cli, + "inspect_requirement", + return_value=failure, + ), + self.assertRaises(typer.BadParameter) as raised, + ): + benchmark_cli._require_benchmark_dependencies("scene") + + self.assertIn( + 'pip install "vidxp[scene]"', + str(raised.exception), + ) + + def test_hirest_dependency_hint_includes_benchmark_parser(self): + registry = Mock() + registry.requirements_for.return_value = () + failure = { + "name": "srt", + "requirement": "srt<4,>=3.5", + "installed_version": None, + "ok": False, + "error": "distribution is not installed", + } + with ( + patch.object( + benchmark_cli, + "create_capability_registry", + return_value=registry, + ), + patch.object( + benchmark_cli, + "inspect_requirement", + return_value=failure, + ), + self.assertRaises(typer.BadParameter) as raised, + ): + benchmark_cli._require_benchmark_dependencies( + "dialogue", + include_benchmark_extra=True, + ) + + self.assertIn( + 'pip install "vidxp[dialogue,benchmarks]"', + str(raised.exception), + ) + + def test_invalid_hirest_pair_json_is_reported_as_input_error(self): + with TemporaryDirectory() as directory: + pairs = Path(directory) / "pairs.json" + pairs.write_text("{", encoding="utf-8") + + with self.assertRaises(typer.BadParameter) as raised: + benchmark_cli._pair_file(pairs) + + self.assertIn("valid readable JSON", str(raised.exception)) + self.assertEqual(raised.exception.param_hint, "--pairs") + + def test_invalid_didemo_media_override_is_reported_as_input_error(self): + with TemporaryDirectory() as directory: + overrides = Path(directory) / "media-overrides.json" + overrides.write_text( + json.dumps({"broken.mp4": "missing.webm"}), + encoding="utf-8", + ) + + with self.assertRaises(typer.BadParameter) as raised: + benchmark_cli._media_override_file(overrides) + + self.assertIn("was not found", str(raised.exception)) + self.assertEqual(raised.exception.param_hint, "--media-overrides") + + def test_hirest_rejects_closed_temporal_fraction_before_dependencies(self): + runner = CliRunner() + service = Mock() + service.index_directory = Path("chroma_data") + with TemporaryDirectory() as directory: + root = Path(directory) + config = root / "repositories.json" + required_files = { + name: root / name + for name in ( + "ground-truth.json", + "categories.json", + "evaluator.py", + "asr.zip", + ) + } + for path in required_files.values(): + path.write_text("", encoding="utf-8") + asr_directory = root / "asr" + asr_directory.mkdir() + with ( + patch.object( + cli, + "create_local_application", + return_value=LocalApplicationContext( + application=service, + jobs=Mock(), + repositories=RepositoryRegistry(config), + repository=RepositoryConfig( + "default", + Path("chroma_data"), + device="cpu", + configured=False, + ), + ), + ), + patch.object( + benchmark_cli, + "_require_benchmark_dependencies", + ) as dependencies, + ): + response = runner.invoke( + cli.app, + [ + "--config", + str(config), + "benchmark", + "hirest", + "--ground-truth", + str(required_files["ground-truth.json"]), + "--categories", + str(required_files["categories.json"]), + "--evaluator", + str(required_files["evaluator.py"]), + "--asr-archive", + str(required_files["asr.zip"]), + "--asr-directory", + str(asr_directory), + "--run-id", + "run-1", + "--temporal-window-fraction", + "1", + ], + ) + + self.assertEqual(response.exit_code, 2, response.output) + self.assertIn("less than one", response.output) + dependencies.assert_not_called() if __name__ == "__main__": diff --git a/tests/test_benchmark_prepare.py b/tests/test_benchmark_prepare.py new file mode 100644 index 0000000..134e67a --- /dev/null +++ b/tests/test_benchmark_prepare.py @@ -0,0 +1,385 @@ +import hashlib +import json +import unittest +import zipfile +from dataclasses import replace +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import Mock, patch + +from typer.testing import CliRunner + +from vidxp import cli +from vidxp.benchmarks.prepare import ( + DIDEMO_KNOWN_REPLACEMENT_VIDEO, + DIDEMO_KNOWN_REPLACEMENT_URL, + PreparationPlan, + PreparationResource, + _didemo_aws_url, + _extract_hirest_asr, + execute_preparation, + plan_didemo, + plan_hirest, +) +from vidxp.composition import LocalApplicationContext +from vidxp.repositories import RepositoryConfig, RepositoryRegistry + + +def _annotation(video: str, annotation_id: int = 1) -> dict: + return { + "annotation_id": annotation_id, + "description": "a visible action", + "num_segments": 6, + "times": [[0, 0]], + "video": video, + } + + +def _resolve_sizes(resources): + return tuple( + replace(resource, size_bytes=resource.size_bytes or 123) + for resource in resources + ) + + +class BenchmarkPreparationTests(unittest.TestCase): + def test_didemo_aws_url_uses_official_yfcc_mapping(self): + url = _didemo_aws_url( + "owner_4253489686_source.m4v", + {"4253489686": "abcdef012345"}, + ) + + self.assertEqual( + url, + "https://multimedia-commons.s3-us-west-2.amazonaws.com/" + "data/videos/mp4/abc/def/abcdef012345.mp4", + ) + + def test_didemo_plan_is_subset_aware_and_copy_paste_ready(self): + first = "owner_4253489686_source.m4v" + second = "owner_7071386095_source.mpg" + annotations = json.dumps( + [_annotation(first), _annotation(second, 2)] + ).encode() + + def fetched(url, _checksum, _name): + if url.endswith("test_data.json"): + return annotations + if url.endswith("yfcc100m_hash.txt"): + return ( + b"4253489686\tabcdef012345\n" + b"7071386095\t123456abcdef\n" + ) + return b"evaluator" + + with ( + TemporaryDirectory() as directory, + patch( + "vidxp.benchmarks.prepare.inspect_media_runtime", + return_value=Mock(ready=True, errors=()), + ), + patch( + "vidxp.benchmarks.prepare._fetch_verified", + side_effect=fetched, + ), + patch( + "vidxp.benchmarks.prepare._with_remote_sizes", + side_effect=_resolve_sizes, + ), + patch( + "vidxp.benchmarks.prepare._valid_media", + return_value=False, + ), + patch( + "vidxp.benchmarks.prepare._valid_artifact", + return_value=False, + ), + ): + plan = plan_didemo( + root=directory, + split="test", + annotation_indices=[1], + ffprobe="ffprobe", + ffmpeg="ffmpeg", + ) + + self.assertEqual(plan.selected_count, 1) + self.assertEqual(plan.selected_video_names, (second,)) + media = [ + resource + for resource in plan.resources + if resource.kind == "media" + ] + self.assertEqual(len(media), 1) + self.assertIn("123/456/123456abcdef.mp4", media[0].url) + self.assertIn("--annotation-indices 1", plan.command) + self.assertIn("--media-directory", plan.command) + + def test_known_didemo_replacement_is_explicit_and_manifested(self): + annotations = json.dumps( + [_annotation(DIDEMO_KNOWN_REPLACEMENT_VIDEO)] + ).encode() + + def fetched(url, _checksum, _name): + if url.endswith("test_data.json"): + return annotations + if url.endswith("yfcc100m_hash.txt"): + return b"13482799053\tdeb3d8c8aba7077b378d16b236b0a5\n" + return b"evaluator" + + with ( + TemporaryDirectory() as directory, + patch( + "vidxp.benchmarks.prepare.inspect_media_runtime", + return_value=Mock(ready=True, errors=()), + ), + patch( + "vidxp.benchmarks.prepare._fetch_verified", + side_effect=fetched, + ), + patch( + "vidxp.benchmarks.prepare._with_remote_sizes", + side_effect=_resolve_sizes, + ), + patch( + "vidxp.benchmarks.prepare._valid_media", + return_value=False, + ), + patch( + "vidxp.benchmarks.prepare._valid_artifact", + return_value=False, + ), + ): + plan = plan_didemo( + root=directory, + split="test", + annotation_indices=[0], + ffprobe="ffprobe", + ffmpeg="ffmpeg", + ) + + replacement = next( + resource + for resource in plan.resources + if resource.replacement_for is not None + ) + self.assertEqual(replacement.url, DIDEMO_KNOWN_REPLACEMENT_URL) + self.assertEqual( + replacement.replacement_for, + DIDEMO_KNOWN_REPLACEMENT_VIDEO, + ) + self.assertIsNotNone(plan.media_overrides_path) + self.assertIn("--media-overrides", plan.command) + + def test_hirest_plan_prepares_released_asr_without_video_downloads(self): + ground_truth = { + "Make tea": { + "tea.mp4": { + "clip": True, + "bounds": [1, 2], + "v_duration": 3, + }, + "other.mp4": { + "clip": True, + "bounds": [0, 1], + "v_duration": 2, + }, + } + } + + def fetched(url, _checksum, _name): + if "all_data_val.json" in url: + return json.dumps(ground_truth).encode() + if url.endswith("categories.json"): + return b"{}" + return b"evaluator" + + with ( + TemporaryDirectory() as directory, + patch( + "vidxp.benchmarks.prepare._fetch_verified", + side_effect=fetched, + ), + patch( + "vidxp.benchmarks.prepare._with_remote_sizes", + side_effect=_resolve_sizes, + ), + patch( + "vidxp.benchmarks.prepare._valid_artifact", + return_value=False, + ), + ): + plan = plan_hirest( + root=directory, + split="validation", + pairs=[("Make tea", "tea.mp4")], + ) + + self.assertEqual(plan.selected_count, 1) + self.assertEqual(plan.selected_video_names, ("tea.mp4",)) + self.assertFalse( + any(resource.kind == "media" for resource in plan.resources) + ) + self.assertGreater(plan.additional_bytes, 17_000_000) + self.assertIn("--asr-archive", plan.command) + self.assertIn("--asr-directory", plan.command) + self.assertIn("--pairs", plan.command) + + def test_hirest_extraction_keeps_only_selected_transcripts(self): + with TemporaryDirectory() as directory: + root = Path(directory) + archive = root / "ASR.zip" + with zipfile.ZipFile(archive, "w") as bundle: + bundle.writestr("ASR/first.srt", "first") + bundle.writestr("ASR/second.srt", "second") + destination = root / "asr" + + extracted = _extract_hirest_asr( + archive, + destination, + ["first.mp4"], + ) + + self.assertEqual(extracted, 1) + self.assertEqual( + (destination / "first.srt").read_text(), + "first", + ) + self.assertFalse((destination / "second.srt").exists()) + + def test_execution_writes_verified_artifacts_and_manifest(self): + with TemporaryDirectory() as directory: + root = Path(directory) + content = b"official" + destination = root / "annotations.json" + plan = PreparationPlan( + benchmark="didemo", + split="test", + root=root, + resources=( + PreparationResource( + name="annotations", + url="https://example.invalid/annotations", + destination=destination, + size_bytes=len(content), + kind="artifact", + content=content, + expected_sha256=hashlib.sha256( + content + ).hexdigest(), + ), + ), + selected_count=1, + selected_video_names=("video.mp4",), + command="vidxp benchmark didemo ...", + manifest_path=root / "preparation-manifest.json", + ) + + result = execute_preparation(plan) + + self.assertEqual(result["status"], "ready") + self.assertEqual(destination.read_bytes(), content) + self.assertTrue(plan.manifest_path.is_file()) + + def test_complete_partial_download_is_verified_without_network_retry(self): + with TemporaryDirectory() as directory: + root = Path(directory) + content = b"resume" + destination = root / "ASR.zip" + partial = destination.with_name(destination.name + ".part") + partial.write_bytes(content) + plan = PreparationPlan( + benchmark="didemo", + split="test", + root=root, + resources=( + PreparationResource( + name="archive", + url="https://example.invalid/ASR.zip", + destination=destination, + size_bytes=len(content), + kind="artifact", + expected_sha256=hashlib.sha256( + content + ).hexdigest(), + ), + ), + selected_count=1, + selected_video_names=(), + command="vidxp benchmark didemo ...", + manifest_path=root / "preparation-manifest.json", + ) + + with patch( + "vidxp.benchmarks.prepare.urllib.request.urlopen" + ) as urlopen: + result = execute_preparation(plan) + + urlopen.assert_not_called() + self.assertEqual(result["status"], "ready") + self.assertEqual(destination.read_bytes(), content) + + +class BenchmarkPreparationCliTests(unittest.TestCase): + def test_prepare_command_prints_generated_run_command(self): + runner = CliRunner() + service = Mock() + with TemporaryDirectory() as directory: + root = Path(directory) + config = root / "repositories.json" + plan = PreparationPlan( + benchmark="didemo", + split="test", + root=root / "prepared", + resources=(), + selected_count=1, + selected_video_names=("video.mp4",), + command="vidxp benchmark didemo --run-id ready", + manifest_path=root / "prepared" / "manifest.json", + ) + with ( + patch.object( + cli, + "create_local_application", + return_value=LocalApplicationContext( + application=service, + jobs=Mock(), + repositories=RepositoryRegistry(config), + repository=RepositoryConfig( + "default", + root / "repository", + device="cpu", + configured=False, + ), + ), + ), + patch( + "vidxp.benchmarks.cli.plan_didemo", + return_value=plan, + ), + patch( + "vidxp.benchmarks.cli.execute_preparation", + return_value={"status": "ready"}, + ), + ): + response = runner.invoke( + cli.app, + [ + "--config", + str(config), + "benchmark", + "prepare", + "didemo", + "--annotation-indices", + "0", + "--yes", + ], + ) + + self.assertEqual(response.exit_code, 0, response.output) + self.assertIn("Benchmark inputs are ready.", response.output) + self.assertIn(plan.command, response.output) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index 055e3cb..42303db 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -1,11 +1,16 @@ import hashlib import json import unittest +from uuid import UUID from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import patch -from vidxp.benchmarks.common import verify_artifact +from vidxp.benchmarks.common import ( + benchmark_generation_id, + benchmark_media_id, + verify_artifact, +) from vidxp.benchmarks.didemo import ( DIDEMO_MOMENTS, _result_classification as didemo_result_classification, @@ -26,10 +31,16 @@ from vidxp.capabilities.schemas import SearchHit +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" + + def scene_hit(chunk, score): return SearchHit( rank=chunk + 1, - video_id="video", + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, start=chunk * 5.0, end=chunk * 5.0 + 1.0, score=score, @@ -43,7 +54,9 @@ def scene_hit(chunk, score): def timed_hit(start, end, score, rank=1): return SearchHit( rank=rank, - video_id="video", + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, start=start, end=end, score=score, @@ -55,6 +68,34 @@ def timed_hit(start, end, score, rank=1): class BenchmarkCommonTests(unittest.TestCase): + def test_generation_identity_is_stable_and_run_scoped(self): + first = benchmark_generation_id("hirest", "validation", "run-1") + + self.assertEqual( + first, + benchmark_generation_id("hirest", "validation", "run-1"), + ) + self.assertEqual(len(first), 32) + self.assertEqual(UUID(hex=first).version, 4) + self.assertNotEqual( + first, + benchmark_generation_id("hirest", "validation", "run-2"), + ) + self.assertNotEqual( + first, + benchmark_generation_id("didemo", "validation", "run-1"), + ) + + def test_official_video_key_maps_to_stable_media_id(self): + first = benchmark_media_id("hirest", "video.mp4") + + self.assertEqual(first, benchmark_media_id("hirest", "video.mp4")) + self.assertEqual(UUID(hex=first).version, 4) + self.assertNotEqual( + first, + benchmark_media_id("didemo", "video.mp4"), + ) + def test_artifact_checksum_is_verified(self): with TemporaryDirectory() as directory: path = Path(directory) / "artifact.json" @@ -79,6 +120,23 @@ def test_artifact_checksum_is_verified(self): revision="abc123", ) + def test_artifact_accepts_declared_line_ending_variants(self): + with TemporaryDirectory() as directory: + path = Path(directory) / "evaluator.py" + path.write_bytes(b"print('official')\r\n") + raw_sha = hashlib.sha256(b"print('official')\n").hexdigest() + checkout_sha = hashlib.sha256(path.read_bytes()).hexdigest() + + artifact = verify_artifact( + path, + name="test evaluator", + expected_sha256=(raw_sha, checkout_sha), + source="https://example.invalid/evaluator.py", + revision="abc123", + ) + + self.assertEqual(artifact["sha256"], checkout_sha) + class DiDeMoAdapterTests(unittest.TestCase): def test_media_override_is_disclosed_in_result_classification(self): @@ -205,11 +263,11 @@ def test_held_out_test_writes_unscored_submission(self): patch( "vidxp.benchmarks.hirest.run_index", return_value={}, - ), + ) as run_index, patch( "vidxp.benchmarks.hirest._generate_predictions", return_value=predictions, - ), + ) as generate_predictions, patch( "vidxp.benchmarks.hirest._evaluate_predictions" ) as evaluate_predictions, @@ -233,6 +291,22 @@ def test_held_out_test_writes_unscored_submission(self): ) self.assertFalse((run_directory / "metrics.json").exists()) evaluate_predictions.assert_not_called() + index_config = run_index.call_args.args[1] + prediction_config = ( + generate_predictions.call_args.kwargs["config"] + ) + self.assertEqual( + index_config.generation_id, + benchmark_generation_id( + "hirest", + "test", + "test-submission", + ), + ) + self.assertEqual( + prediction_config.generation_id, + index_config.generation_id, + ) @patch("vidxp.benchmarks.hirest.run_logged_evaluator") def test_official_evaluator_is_forced_to_utf8( diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 07847ec..8a9d8b5 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -1,25 +1,28 @@ import unittest +from importlib.metadata import PackageNotFoundError +from types import SimpleNamespace +from unittest.mock import Mock, patch from pydantic import BaseModel, ValidationError +from vidxp.capabilities.actor.config import ActorConfig from vidxp.capabilities.contracts import ( - CapabilityContext, CapabilityDefinition, + CapabilityExecutor, CapabilityIndexResult, CapabilityInput, CapabilityOutput, + CapabilityPlugin, + CapabilityProvenance, OperationDefinition, + RuntimeCheck, ) -from vidxp.capabilities.actor.config import ActorConfig from vidxp.capabilities.dialogue.config import DialogueConfig from vidxp.capabilities.registry import ( - CAPABILITIES, - capability_names, - collection_names, - index_capability_names, - preparable_capability_names, - validate_capability_options, + CapabilityRegistry, + create_capability_registry, ) +from vidxp.capability_service import CapabilityService from vidxp.capabilities.scene.config import SceneConfig from vidxp.core.contracts import IndexConfig from vidxp.core.runner import _index_groups @@ -34,114 +37,139 @@ class ExampleOutput(CapabilityOutput): class CapabilityTests(unittest.TestCase): - def test_registry_is_explicit_and_drives_index_collections(self): + def setUp(self): + self.registry = create_capability_registry() + + def test_registry_drives_capability_metadata(self): self.assertEqual( - capability_names(), + self.registry.names(), ("dialogue", "scene", "actor"), ) + self.assertEqual(self.registry.index_names(), self.registry.names()) self.assertEqual( - index_capability_names(), - capability_names(), - ) - self.assertEqual( - preparable_capability_names(), - ("dialogue", "scene"), + self.registry.preparable_names(), + ("dialogue", "scene", "actor"), ) self.assertEqual( - collection_names(), + self.registry.collection_names(), { "dialogue": "dialogue", "scene": "scene", "actor": "actor", }, ) - self.assertEqual(collection_names(("scene",)), {"scene": "scene"}) - def test_registered_operations_use_pydantic_contracts(self): - for capability in CAPABILITIES.values(): - self.assertIsInstance(capability, BaseModel) - for operation in capability.operations.values(): - self.assertIsInstance(operation, BaseModel) - self.assertTrue( - issubclass(operation.input_model, BaseModel) - ) - self.assertTrue( - issubclass(operation.output_model, BaseModel) - ) + def test_registered_operations_are_schema_only_metadata(self): + self.assertNotIn("handler", OperationDefinition.model_fields) + for definition in self.registry.definitions.values(): + self.assertIsInstance(definition, BaseModel) + for operation in definition.operations.values(): + self.assertTrue(issubclass(operation.input_model, BaseModel)) + self.assertTrue(issubclass(operation.output_model, BaseModel)) + + def test_internal_actor_cluster_lookup_is_not_advertised(self): + operations = { + operation.name + for operation in CapabilityService(self.registry) + .get("actor") + .operations + } + + self.assertNotIn("cluster", operations) + self.assertIn("clusters", operations) + self.assertIn("detections", operations) + + def test_capability_list_is_summary_only(self): + service = CapabilityService(self.registry) + + summaries = service.list() + detail = service.get("scene") + + self.assertEqual( + tuple(summary.name for summary in summaries), + self.registry.names(), + ) + self.assertTrue(detail.operations) + self.assertTrue( + all(not hasattr(summary, "operations") for summary in summaries) + ) - def test_capability_contracts_are_frozen_and_schema_validated(self): + def test_executor_handlers_match_declared_operations(self): + for name, definition in self.registry.definitions.items(): + self.assertEqual( + set(self.registry.executor(name).operations), + set(definition.operations), + ) + + def test_contracts_are_frozen_and_index_metadata_is_complete(self): with self.assertRaises(ValidationError): - CAPABILITIES["scene"].name = "changed" + self.registry.get("scene").name = "changed" with self.assertRaises(ValidationError): CapabilityDefinition( name="broken", description="Incomplete index integration.", extra="broken", collection_name="broken", - indexer=lambda **_: None, ) with self.assertRaises(ValidationError): - CapabilityIndexResult( - summary={}, - timings={"index": -1}, - ) - - def test_built_in_settings_are_capability_owned_and_validated(self): - self.assertIs(CAPABILITIES["dialogue"].config_model, DialogueConfig) - self.assertIs(CAPABILITIES["scene"].config_model, SceneConfig) - self.assertIs(CAPABILITIES["actor"].config_model, ActorConfig) + CapabilityIndexResult(summary={}, timings={"index": -1}) - options = validate_capability_options( - ("scene",), - {"scene": {"batch_size": 4, "model": "test-model"}}, + def test_built_in_settings_are_owned_and_validated(self): + self.assertIs( + self.registry.get("dialogue").config_model, + DialogueConfig, ) + self.assertIs(self.registry.get("scene").config_model, SceneConfig) + self.assertIs(self.registry.get("actor").config_model, ActorConfig) - self.assertEqual( - options["scene"], - {"batch_size": 4, "model": "test-model"}, + options = self.registry.validate_options( + ("scene",), + {"scene": {"batch_size": 4, "sample_fps": 0.5}}, ) + self.assertEqual(options["scene"]["batch_size"], 4) + self.assertEqual(options["scene"]["sample_fps"], 0.5) + self.assertNotIn("model", options["scene"]) + with self.assertRaises(ValidationError): + self.registry.validate_options( + ("scene",), + {"scene": {"sample_fps": 0}}, + ) + with self.assertRaises(ValidationError): + self.registry.validate_options( + ("scene",), + {"scene": {"model": "unapproved/model"}}, + ) with self.assertRaises(ValidationError): - validate_capability_options( + self.registry.validate_options( ("actor",), {"actor": {"match_threshold": 2}}, ) - def test_core_config_has_no_built_in_capability_fields(self): - fields = IndexConfig.__dataclass_fields__ - for name in ( - "sentence_model", - "whisper_model", - "clip_model", - "dialogue_batch_size", - "scene_batch_size", - "actor_batch_size", - "face_match_threshold", - ): - self.assertNotIn(name, fields) - - def test_operation_validates_both_input_and_output(self): - operation = OperationDefinition( - input_model=ExampleInput, - output_model=ExampleOutput, - handler=lambda _context, request: { - "doubled": request.value * 2 + def test_registry_rejects_executor_metadata_drift(self): + definition = CapabilityDefinition( + name="export", + description="Export.", + extra="export", + operations={ + "run": OperationDefinition( + input_model=ExampleInput, + output_model=ExampleOutput, + requires_index=False, + ) }, - requires_index=False, ) - - result = operation.invoke( - CapabilityContext(config=None), - {"value": 3}, - ) - - self.assertEqual(result, ExampleOutput(doubled=6)) - with self.assertRaises(ValidationError): - operation.invoke( - CapabilityContext(config=None), - {"value": 3, "unexpected": True}, + registry = CapabilityRegistry( + ( + CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor(), + ), ) + ) + with self.assertRaisesRegex(RuntimeError, "operation handlers"): + registry.executor("export") - def test_operation_only_capability_needs_no_dummy_indexer(self): + def test_operation_only_capability_needs_no_index_metadata(self): capability = CapabilityDefinition( name="export", description="Export results.", @@ -150,37 +178,274 @@ def test_operation_only_capability_needs_no_dummy_indexer(self): "run": OperationDefinition( input_model=ExampleInput, output_model=ExampleOutput, - handler=lambda _context, request: { - "doubled": request.value * 2 - }, requires_index=False, ) }, ) - - self.assertIsNone(capability.indexer) self.assertIsNone(capability.collection_name) + self.assertIsNone(capability.execution_group) - def test_shared_visual_handler_is_grouped_without_name_switches(self): + def test_visual_execution_group_is_explicit(self): self.assertEqual( - _index_groups(("dialogue", "scene", "actor")), + _index_groups( + ("dialogue", "scene", "actor"), + self.registry, + ), (("dialogue",), ("scene", "actor")), ) - self.assertIsNotNone(CAPABILITIES["scene"].index_processor) - self.assertIsNotNone(CAPABILITIES["actor"].index_processor) - self.assertIsNone(CAPABILITIES["dialogue"].index_processor) + self.assertIsNotNone( + self.registry.executor("scene").index_processor + ) + self.assertIsNotNone( + self.registry.executor("actor").index_processor + ) + self.assertIsNone( + self.registry.executor("dialogue").index_processor + ) + + def test_core_config_has_no_provider_specific_fields(self): + fields = IndexConfig.__dataclass_fields__ + for name in ( + "sentence_model", + "whisper_model", + "clip_model", + "actor_batch_size", + "face_match_threshold", + ): + self.assertNotIn(name, fields) - def test_capability_options_do_not_require_core_config_fields(self): - config = IndexConfig( - enabled_modalities=("ocr",), - collection_names={"ocr": "ocr"}, - capability_options={"ocr": {"language": "en"}}, + def test_external_plugins_require_exact_distribution_entry_point_pair(self): + plugin = CapabilityPlugin( + definition=CapabilityDefinition( + name="ocr", + description="OCR.", + extra="ocr", + operations={ + "run": OperationDefinition( + input_model=ExampleInput, + output_model=ExampleOutput, + requires_index=False, + ) + }, + ), + executor_factory=lambda: CapabilityExecutor( + operations={ + "run": lambda _context, request: { + "doubled": request.value * 2 + } + } + ), + requirements=("example-runtime>=1,<2",), + ) + distribution = SimpleNamespace( + name="acme-capabilities", + version="1.2.3", + ) + entry_point = SimpleNamespace( + name="ocr", + dist=distribution, + load=Mock(return_value=plugin), ) + with patch( + "vidxp.capabilities.registry.entry_points", + return_value=(entry_point,), + ): + rejected = create_capability_registry( + external=True, + allowlist=("acme-capabilities:other",), + ) + accepted = create_capability_registry( + external=True, + allowlist=("acme-capabilities:ocr",), + ) + self.assertNotIn("ocr", rejected.names()) + self.assertIn("ocr", accepted.names()) self.assertEqual( - config.options_for("ocr"), - {"language": "en"}, + accepted.provenance("ocr"), + CapabilityProvenance( + distribution="acme-capabilities", + entry_point="ocr", + version="1.2.3", + ), + ) + self.assertEqual( + [item.name for item in accepted.requirements_for(("ocr",))], + ["example-runtime"], + ) + with patch( + "vidxp.dependencies.version", + side_effect=PackageNotFoundError("example-runtime"), + ): + check = accepted.dependency_checks(("ocr",))[0] + self.assertFalse(check.ok) + self.assertEqual(check.capability, "ocr") + self.assertEqual(check.provenance.distribution, "acme-capabilities") + self.assertEqual(entry_point.load.call_count, 1) + + def test_plugin_contract_and_collision_errors_include_provenance(self): + provenance = CapabilityProvenance( + distribution="acme-capabilities", + entry_point="ocr", + version="1.2.3", + ) + invalid = CapabilityPlugin( + definition=CapabilityDefinition( + name="ocr", + description="OCR.", + extra="ocr", + operations={ + "run": OperationDefinition( + input_model=ExampleInput, + output_model=ExampleOutput, + requires_index=False, + ) + }, + ), + executor_factory=lambda: CapabilityExecutor( + operations={"run": lambda _context, _request: {"doubled": 2}} + ), + contract_version=999, + provenance=provenance, ) + with self.assertRaisesRegex( + RuntimeError, + "acme-capabilities:ocr", + ): + CapabilityRegistry((invalid,)) + + collision = invalid.model_copy( + update={ + "definition": self.registry.get("scene"), + "contract_version": 1, + } + ) + built_in = CapabilityPlugin( + definition=self.registry.get("scene"), + executor_factory=lambda: CapabilityExecutor(), + ) + with self.assertRaisesRegex( + RuntimeError, + "built-in VidXP capability.*acme-capabilities:ocr", + ): + CapabilityRegistry((built_in, collision)) + + def test_external_plugin_loading_is_deterministic_and_errors_are_chained(self): + def plugin(name): + return CapabilityPlugin( + definition=CapabilityDefinition( + name=name, + description=f"{name} capability.", + extra=name, + operations={ + "run": OperationDefinition( + input_model=ExampleInput, + output_model=ExampleOutput, + requires_index=False, + ) + }, + ), + executor_factory=lambda: CapabilityExecutor( + operations={ + "run": lambda _context, request: { + "doubled": request.value * 2 + } + } + ), + ) + + distribution = SimpleNamespace(name="acme", version="1") + zeta = SimpleNamespace( + name="zeta", + dist=distribution, + load=Mock(return_value=plugin("zeta")), + ) + alpha = SimpleNamespace( + name="alpha", + dist=distribution, + load=Mock(return_value=plugin("alpha")), + ) + with patch( + "vidxp.capabilities.registry.entry_points", + return_value=(zeta, alpha), + ): + registry = create_capability_registry( + external=True, + allowlist=("acme:zeta", "acme:alpha"), + ) + self.assertEqual(registry.names()[-2:], ("alpha", "zeta")) + + failure = RuntimeError("factory failure") + broken = SimpleNamespace( + name="broken", + dist=distribution, + load=Mock(side_effect=failure), + ) + with ( + patch( + "vidxp.capabilities.registry.entry_points", + return_value=(broken,), + ), + self.assertRaisesRegex(RuntimeError, "acme:broken") as raised, + ): + create_capability_registry( + external=True, + allowlist=("acme:broken",), + ) + self.assertIs(raised.exception.__cause__, failure) + + def test_external_runtime_check_errors_are_sanitized_with_provenance(self): + events = [] + + def fail_runtime_check(): + events.append("inspect") + raise RuntimeError("token=do-not-leak") + + provenance = CapabilityProvenance( + distribution="acme-capabilities", + entry_point="ocr", + version="1.2.3", + ) + plugin = CapabilityPlugin( + definition=CapabilityDefinition( + name="ocr", + description="OCR.", + extra="ocr", + operations={ + "run": OperationDefinition( + input_model=ExampleInput, + output_model=ExampleOutput, + requires_index=False, + ) + }, + ), + executor_factory=lambda: CapabilityExecutor( + operations={ + "run": lambda _context, request: { + "doubled": request.value * 2 + } + }, + runtime_checks=( + RuntimeCheck( + label="ocr runtime", + check=fail_runtime_check, + ), + ), + ), + provenance=provenance, + ) + + check = CapabilityRegistry((plugin,)).dependency_checks( + ("ocr",), + on_check_start=lambda capability, _kind, name: events.append( + (capability, name) + ), + )[0] + + self.assertEqual(events, [("ocr", "ocr runtime"), "inspect"]) + self.assertEqual(check.error, "runtime check failed") + self.assertNotIn("do-not-leak", check.model_dump_json()) + self.assertEqual(check.provenance, provenance) if __name__ == "__main__": diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 33fa14e..fb22c7d 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -12,6 +12,10 @@ from vidxp.capabilities.schemas import SearchHit, SearchResult +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" + + class ContractTests(unittest.TestCase): def test_stable_source_ids_escape_delimiters_without_collisions(self): first = stable_source_id("run:a", "video", "scene", "1") @@ -23,6 +27,15 @@ def test_stable_source_ids_escape_delimiters_without_collisions(self): stable_source_id("r", "فيديو:1", "scene", "0"), ) self.assertEqual(first.count(":"), 3) + scoped = stable_source_id( + "run:a", + "video", + "scene", + "1", + generation_id=GENERATION_ID, + ) + self.assertNotEqual(first, scoped) + self.assertEqual(scoped.count(":"), 4) def test_config_is_validated_and_run_paths_are_isolated(self): with TemporaryDirectory() as directory: @@ -59,6 +72,17 @@ def test_config_is_validated_and_run_paths_are_isolated(self): enabled_modalities=("scene",), ) self.assertEqual(first.fingerprint(), relocated.fingerprint()) + generation_directory = Path(directory) / "generation" + generated = IndexConfig.local( + storage_directory=Path(directory) / "store", + generation_directory=generation_directory, + generation_id="generation-1", + ) + self.assertEqual(generated.run_directory, generation_directory) + self.assertEqual( + generated.index_directory, + Path(directory) / "store", + ) def test_path_objects_are_normalized_for_manifest_serialization(self): config = IndexConfig.local( @@ -116,7 +140,9 @@ def test_invalid_config_is_rejected(self): def test_result_contract_serializes_complete_ranked_hits(self): hit = SearchHit( rank=1, - video_id="video-1", + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, start=1.0, end=2.0, score=-0.2, @@ -132,7 +158,7 @@ def test_result_contract_serializes_complete_ranked_hits(self): hits=(hit,), ) - self.assertEqual(result.to_prediction()["q1"][0]["video_id"], "video-1") + self.assertEqual(result.to_prediction()["q1"][0]["video_id"], MEDIA_ID) self.assertEqual(result.to_dict()["hits"][0]["raw_distance"], 0.2) def test_cancellation_is_cooperative(self): diff --git a/tests/test_cursors.py b/tests/test_cursors.py new file mode 100644 index 0000000..5b86e81 --- /dev/null +++ b/tests/test_cursors.py @@ -0,0 +1,56 @@ +import base64 +import unittest + +from vidxp.core.cursors import ( + MAX_CURSOR_OFFSET, + CursorError, + decode_cursor, + decode_offset_cursor, + encode_cursor, + encode_offset_cursor, +) + + +class CursorTests(unittest.TestCase): + def test_cursor_rejects_noncanonical_or_malformed_base64(self): + cursor = encode_cursor("test", {"offset": 1}) + + for invalid in ( + cursor.rstrip("="), + f"{cursor}=", + f" {cursor}", + cursor.replace("-", "+").replace("_", "/"), + "not-base64!", + ): + if invalid == cursor: + continue + with self.subTest(cursor=invalid): + with self.assertRaises(CursorError): + decode_cursor(invalid, "test") + + decoded = base64.urlsafe_b64decode(cursor) + noncanonical = base64.b64encode(decoded).decode() + if noncanonical != cursor: + with self.assertRaises(CursorError): + decode_cursor(noncanonical, "test") + + def test_offset_cursor_is_bounded_to_signed_database_bigint(self): + cursor = encode_offset_cursor(MAX_CURSOR_OFFSET, scope="test") + self.assertEqual( + decode_offset_cursor(cursor, scope="test"), + MAX_CURSOR_OFFSET, + ) + + with self.assertRaises(CursorError): + encode_offset_cursor(MAX_CURSOR_OFFSET + 1, scope="test") + + oversized = encode_cursor( + "test", + {"offset": MAX_CURSOR_OFFSET + 1}, + ) + with self.assertRaises(CursorError): + decode_offset_cursor(oversized, scope="test") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dbos_jobs.py b/tests/test_dbos_jobs.py new file mode 100644 index 0000000..fb977cf --- /dev/null +++ b/tests/test_dbos_jobs.py @@ -0,0 +1,558 @@ +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from threading import Event +from time import sleep +from unittest.mock import ANY, Mock + +from dbos import DBOS + +from vidxp.application_models import ( + ApplicationError, + CreateIndexCommand, + ErrorCategory, + FusedSearchResult, + FusionProvenance, + IndexSnapshotReference, + IndexResult, + JobKind, + JobQueue, + JobState, + ListJobsCommand, + QueryAnswer, + QueryAnswerMode, + QueryJobRequest, + QueryPlan, + QueryVideoCommand, + SearchCommand, + SearchHit, + SearchJobRequest, + SearchMomentsPlanStep, + SearchResult, +) +from vidxp.core.cursors import MAX_CURSOR_OFFSET, encode_cursor +from vidxp.infrastructure.dbos_jobs import ( + DBOSJobBackend, + _decode_search_result, +) +from vidxp.job_service import JobService +from vidxp.ports import InvalidJobBackendRequestError +from vidxp.settings import VidXPSettings +from vidxp.workflow_contracts import ( + QUEUE_NAMES, + WORKFLOW_CLASS_NAME, + WORKFLOW_INSTANCE_NAME, + WORKFLOW_KINDS, + decode_workflow_request, +) + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" +IDEMPOTENCY_KEY = "423456781234423481234567890abcde" +SNAPSHOT_SHA256 = "a" * 64 + + +class DBOSJobIntegrationTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + from vidxp.infrastructure.dbos_workflows import VidXPWorkerWorkflows + + cls.worker = VidXPWorkerWorkflows(Mock()) + + def setUp(self): + self.directory = TemporaryDirectory(ignore_cleanup_errors=True) + database = Path(self.directory.name) / "jobs.sqlite3" + self.database_url = f"sqlite:///{database.as_posix()}" + DBOS.destroy() + DBOS( + config={ + "name": "vidxp", + "system_database_url": self.database_url, + "application_version": "test-v1", + "executor_id": "test-worker", + "use_listen_notify": False, + } + ) + self.application = Mock() + self.application.create_index.return_value = IndexResult( + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + snapshot_id=SNAPSHOT_ID, + active_media_count=1, + record_counts={"scene": 1}, + ) + self.application.search.return_value = FusedSearchResult( + query_id="fused:taxi", + query="taxi", + modalities=("scene",), + fusion=FusionProvenance( + requested_modalities=("scene",), + searched_modalities=("scene",), + ), + ) + self.application.query_video.return_value = QueryAnswer( + question="What happens?", + mode=QueryAnswerMode.no_evidence, + plan=QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="scene", + query="What happens?", + ), + ) + ), + fusion=FusionProvenance( + requested_modalities=("scene",), + searched_modalities=("scene",), + ), + fallback_reason="no_evidence", + ) + self.worker.application = self.application + DBOS.reset_system_database() + DBOS.listen_queues([QUEUE_NAMES[JobQueue.cpu]]) + DBOS.launch() + DBOS.register_queue( + QUEUE_NAMES[JobQueue.cpu], + worker_concurrency=1, + ) + self.backend = DBOSJobBackend( + system_database_url=self.database_url, + application_version="test-v1", + ) + self.jobs = JobService( + settings=VidXPSettings( + repository_root=self.directory.name, + runtime_backend="cpu", + workflow_poll_interval_seconds=0.01, + ), + backend=self.backend, + read_planner=Mock( + plan_search=Mock( + side_effect=lambda command: SearchJobRequest( + command=command, + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + ), + plan_query=Mock( + side_effect=lambda command: QueryJobRequest( + command=command, + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + ), + ), + ) + + def tearDown(self): + self.backend.close() + DBOS.destroy() + self.directory.cleanup() + + def test_sqlite_queue_persists_progress_and_typed_result(self): + def create_index(command, *, execution): + execution.report( + { + "stage": "frames", + "message": "Indexed one frame.", + "current": 1, + "total": 1, + } + ) + return IndexResult( + media_id=command.media_id, + generation_id=GENERATION_ID, + snapshot_id=SNAPSHOT_ID, + active_media_count=1, + record_counts={"scene": 1}, + ) + + self.application.create_index.side_effect = create_index + submitted = self.jobs.submit_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + ) + completed = self.jobs.wait(submitted.job_id) + + self.assertEqual(completed.state, JobState.succeeded) + self.assertEqual(completed.kind, JobKind.index) + self.assertEqual(completed.progress.stage, "complete") + self.assertEqual( + completed.result.result.generation_id, + GENERATION_ID, + ) + page = self.jobs.list(ListJobsCommand(page_size=1)) + self.assertEqual(page.items[0].job_id, submitted.job_id) + self.application.create_index.assert_called_once() + + def test_health_requires_owned_executor_health(self): + health_check = Mock(side_effect=RuntimeError("worker unavailable")) + self.backend.health_check = health_check + + with self.assertRaisesRegex(RuntimeError, "worker unavailable"): + self.backend.health() + + health_check.assert_called_once_with() + + def test_model_search_runs_in_worker_and_returns_typed_result(self): + submitted = self.jobs.submit_search( + SearchCommand( + modalities=("scene",), + query="taxi", + top_k=1, + ) + ) + completed = self.jobs.wait(submitted.job_id) + + self.assertEqual(completed.state, JobState.succeeded) + self.assertEqual(completed.kind, JobKind.search) + self.assertEqual(completed.result.result.query, "taxi") + self.application.search.assert_called_once_with( + SearchCommand( + modalities=("scene",), + query="taxi", + top_k=1, + ), + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + + def test_grounded_query_runs_in_worker_and_returns_typed_result(self): + command = QueryVideoCommand( + question="What happens?", + modalities=("scene",), + ) + + submitted = self.jobs.submit_query(command) + completed = self.jobs.wait(submitted.job_id) + + self.assertEqual(completed.state, JobState.succeeded) + self.assertEqual(completed.kind, JobKind.query) + self.assertEqual( + completed.result.result.mode, + QueryAnswerMode.no_evidence, + ) + self.application.query_video.assert_called_once_with( + command, + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + execution=ANY, + ) + + def test_legacy_atomic_search_output_remains_readable(self): + legacy = SearchResult( + query_id="scene:legacy", + query="taxi", + modality="scene", + hits=( + SearchHit( + rank=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=1, + end=2, + score=-0.1, + raw_distance=0.1, + modality="scene", + source_id="scene:1", + ), + ), + ) + + decoded = _decode_search_result(legacy.model_dump(mode="json")) + + self.assertEqual(decoded.query, "taxi") + self.assertEqual(decoded.moments[0].hits[0].source_id, "scene:1") + self.assertEqual(WORKFLOW_KINDS["vidxp.search.v1"], JobKind.search) + + def test_legacy_search_request_upgrades_to_the_v2_command(self): + upgraded = decode_workflow_request( + { + "kind": "search", + "command": { + "modality": "scene", + "query": "taxi", + "top_k": 3, + }, + "snapshot": { + "snapshot_id": SNAPSHOT_ID, + "snapshot_sha256": SNAPSHOT_SHA256, + }, + } + ) + + self.assertIsInstance(upgraded, SearchJobRequest) + self.assertEqual(upgraded.command.modalities, ("scene",)) + self.assertEqual(upgraded.command.top_k, 3) + self.assertTrue(hasattr(self.worker, "legacy_search_workflow")) + self.assertTrue(hasattr(self.worker, "run_legacy_search_step")) + + def test_registered_legacy_search_workflow_executes_upgraded_request(self): + legacy_id = "923456781234423481234567890abcde" + self.backend.client.enqueue( + { + "workflow_name": "vidxp.search.v1", + "queue_name": QUEUE_NAMES[JobQueue.cpu], + "workflow_id": legacy_id, + "app_version": "test-v1", + "attributes": {"vidxp_queue": JobQueue.cpu.value}, + "class_name": WORKFLOW_CLASS_NAME, + "instance_name": WORKFLOW_INSTANCE_NAME, + }, + { + "kind": "search", + "command": { + "modality": "scene", + "query": "taxi", + "top_k": 1, + }, + "snapshot": { + "snapshot_id": SNAPSHOT_ID, + "snapshot_sha256": SNAPSHOT_SHA256, + }, + }, + ) + + completed = self.jobs.wait(legacy_id) + + self.assertEqual(completed.state, JobState.succeeded) + self.application.search.assert_called_once_with( + SearchCommand( + modalities=("scene",), + query="taxi", + top_k=1, + ), + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + + def test_failed_legacy_search_retries_as_a_v2_workflow(self): + legacy_id = "a23456781234423481234567890abcde" + self.application.search.side_effect = ( + ApplicationError( + "temporary_search_failure", + ErrorCategory.unavailable, + "The temporary search dependency is unavailable.", + retryable=True, + ), + self.application.search.return_value, + ) + payload = { + "kind": "search", + "command": { + "modality": "scene", + "query": "taxi", + "top_k": 1, + }, + "snapshot": { + "snapshot_id": SNAPSHOT_ID, + "snapshot_sha256": SNAPSHOT_SHA256, + }, + } + self.backend.client.enqueue( + { + "workflow_name": "vidxp.search.v1", + "queue_name": QUEUE_NAMES[JobQueue.cpu], + "workflow_id": legacy_id, + "app_version": "test-v1", + "attributes": {"vidxp_queue": JobQueue.cpu.value}, + "class_name": WORKFLOW_CLASS_NAME, + "instance_name": WORKFLOW_INSTANCE_NAME, + }, + payload, + ) + with self.assertRaises(ApplicationError): + self.jobs.wait(legacy_id) + + retried = self.jobs.retry( + legacy_id, + retry_id=IDEMPOTENCY_KEY, + ) + completed = self.jobs.wait(retried.job_id) + + self.assertEqual(retried.job_id, IDEMPOTENCY_KEY) + self.assertEqual(completed.state, JobState.succeeded) + self.assertEqual(completed.result.result.query, "taxi") + status = self.backend._status(retried.job_id) + self.assertEqual(status.name, "vidxp.search.v2") + + def test_huge_cursor_offset_is_rejected_before_database_access(self): + cursor = encode_cursor( + "vidxp:jobs", + {"offset": MAX_CURSOR_OFFSET + 1}, + ) + self.backend.client = Mock(wraps=self.backend.client) + + with self.assertRaises(InvalidJobBackendRequestError): + self.backend.list( + ListJobsCommand(page_size=1, cursor=cursor) + ) + + self.backend.client.list_workflows.assert_not_called() + + def test_submission_idempotency_replays_only_the_same_request(self): + command = CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + + first = self.jobs.submit_index(command, job_id=IDEMPOTENCY_KEY) + second = self.jobs.submit_index(command, job_id=IDEMPOTENCY_KEY) + completed = self.jobs.wait(first.job_id) + + self.assertEqual(first.job_id, IDEMPOTENCY_KEY) + self.assertEqual(second.job_id, IDEMPOTENCY_KEY) + self.assertEqual(completed.state, JobState.succeeded) + self.application.create_index.assert_called_once() + + with self.assertRaises(ApplicationError) as caught: + self.jobs.submit_index( + command.model_copy(update={"frame_stride": 2}), + job_id=IDEMPOTENCY_KEY, + ) + self.assertEqual(caught.exception.code, "idempotency_key_reused") + self.assertEqual( + caught.exception.category, + ErrorCategory.validation, + ) + + def test_descending_pages_are_frozen_against_new_submissions(self): + original_ids = ( + "523456781234423481234567890abcde", + "623456781234423481234567890abcde", + "723456781234423481234567890abcde", + ) + command = CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + for job_id in original_ids: + self.jobs.submit_index(command, job_id=job_id) + sleep(0.01) + + first = self.jobs.list(ListJobsCommand(page_size=2)) + self.assertIsNotNone(first.next_cursor) + + later_id = "823456781234423481234567890abcde" + sleep(0.01) + self.jobs.submit_index(command, job_id=later_id) + second = self.jobs.list( + ListJobsCommand(page_size=2, cursor=first.next_cursor) + ) + + paged_ids = {job.job_id for job in (*first.items, *second.items)} + self.assertEqual(paged_ids, set(original_ids)) + self.assertNotIn(later_id, paged_ids) + + def test_cancellation_reaches_the_running_application_operation(self): + started = Event() + stopped = Event() + + def create_index(_command, *, execution): + started.set() + try: + while True: + execution.checkpoint() + sleep(0.01) + finally: + stopped.set() + + self.application.create_index.side_effect = create_index + submitted = self.jobs.submit_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + ) + + self.assertTrue(started.wait(timeout=5)) + cancelled = self.jobs.cancel(submitted.job_id) + + self.assertEqual(cancelled.state, JobState.cancelled) + self.assertTrue(stopped.wait(timeout=5)) + + def test_failed_job_retries_idempotently_as_new_typed_execution(self): + attempts = 0 + + def create_index(command, *, execution): + nonlocal attempts + attempts += 1 + if attempts == 1: + raise ApplicationError( + "temporary_index_failure", + ErrorCategory.unavailable, + "The temporary indexing dependency is unavailable.", + retryable=True, + ) + return IndexResult( + media_id=command.media_id, + generation_id=GENERATION_ID, + snapshot_id=SNAPSHOT_ID, + active_media_count=1, + record_counts={"scene": 1}, + ) + + self.application.create_index.side_effect = create_index + submitted = self.jobs.submit_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + ) + with self.assertRaises(ApplicationError): + self.jobs.wait(submitted.job_id) + + failed = self.jobs.get(submitted.job_id) + steps = self.backend.client.list_workflow_steps( + failed.job_id, + load_output=True, + ) + failed_step = next(step for step in steps if step["error"] is not None) + self.assertEqual( + str(failed_step["error"]), + "VidXP job execution failed.", + ) + self.assertNotIn( + "temporary indexing dependency", + repr(failed_step["error"]).lower(), + ) + retried = self.jobs.retry( + failed.job_id, + retry_id=IDEMPOTENCY_KEY, + ) + replayed = self.jobs.retry( + failed.job_id, + retry_id=IDEMPOTENCY_KEY, + ) + completed = self.jobs.wait(retried.job_id) + + self.assertNotEqual(retried.job_id, failed.job_id) + self.assertEqual(retried.job_id, IDEMPOTENCY_KEY) + self.assertEqual(replayed.job_id, IDEMPOTENCY_KEY) + self.assertEqual(completed.state, JobState.succeeded) + self.assertEqual(completed.queue, JobQueue.cpu) + self.assertEqual( + completed.result.result.generation_id, + GENERATION_ID, + ) + self.assertIsNone(completed.error) + self.assertEqual(attempts, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_generation_manifest.py b/tests/test_generation_manifest.py new file mode 100644 index 0000000..1e10efe --- /dev/null +++ b/tests/test_generation_manifest.py @@ -0,0 +1,190 @@ +import json +import unittest +from copy import deepcopy +from datetime import datetime, timezone +from uuid import uuid1, uuid4 + +from pydantic import ValidationError + +from vidxp.core.contracts import ( + INDEX_SCHEMA_VERSION, + MANIFEST_SCHEMA_VERSION, +) +from vidxp.core.generations import CompletedGenerationManifest + + +SHA256 = "a" * 64 +OTHER_SHA256 = "b" * 64 + + +def completed_manifest() -> dict: + generation_id = uuid4().hex + created_at = datetime(2026, 7, 29, tzinfo=timezone.utc).isoformat() + completed_at = datetime(2026, 7, 29, 0, 1, tzinfo=timezone.utc).isoformat() + return { + "manifest_schema_version": MANIFEST_SCHEMA_VERSION, + "index_schema_version": INDEX_SCHEMA_VERSION, + "dataset": "local", + "split": "local", + "run_id": "default", + "generation_id": generation_id, + "state": "complete", + "created_at": created_at, + "updated_at": completed_at, + "completed_at": completed_at, + "config_fingerprint": SHA256, + "execution_fingerprint": OTHER_SHA256, + "configuration": { + "enabled_modalities": ["scene", "dialogue"], + "frame_stride": 1, + }, + "models": {"runtime": {"requested": "cpu"}}, + "git": {"commit": None, "dirty": None}, + "environment": { + "python": "3.13", + "dependencies": {"chromadb": "1.5.9"}, + }, + "inputs": { + "episode-1": { + "sha256": SHA256, + "checksums": {"video": SHA256}, + "size": 1024, + "source_name": "episode.mp4", + "path": "/media/episode.mp4", + "metadata": {}, + } + }, + "videos": { + "episode-1": { + "state": "complete", + "started_at": created_at, + "stages": { + "scene": { + "video_id": "episode-1", + "stage": "scene", + "seconds": 1.5, + "stats": {"scene_frames": 4}, + "recorded_at": completed_at, + } + }, + "completed_at": completed_at, + "summary": {"processed_frames": 4}, + } + }, + "completed_videos": ["episode-1"], + "failed_videos": [], + "interrupted_videos": [], + "processed_frames": 4, + "record_counts": {"scene": 4, "dialogue": 2}, + "store_size_bytes_at_commit": 4096, + } + + +class CompletedGenerationManifestTests(unittest.TestCase): + def validate(self, payload: dict) -> CompletedGenerationManifest: + return CompletedGenerationManifest.model_validate_json( + json.dumps(payload) + ) + + def test_valid_completed_manifest_is_strict_frozen_and_json_safe(self): + manifest = self.validate(completed_manifest()) + + self.assertEqual(manifest.state, "complete") + self.assertEqual(manifest.record_counts["scene"], 4) + with self.assertRaises(ValidationError): + manifest.state = "running" + + invalid = completed_manifest() + invalid["unexpected"] = True + with self.assertRaises(ValidationError): + self.validate(invalid) + + invalid = completed_manifest() + invalid["store_size_bytes_at_commit"] = "4096" + with self.assertRaises(ValidationError): + CompletedGenerationManifest.model_validate(invalid) + + invalid = completed_manifest() + invalid["models"] = {"runtime": object()} + with self.assertRaises(ValidationError): + CompletedGenerationManifest.model_validate(invalid) + + def test_requires_versions_uuid4_complete_state_and_aware_timestamps(self): + invalid_values = ( + ("manifest_schema_version", MANIFEST_SCHEMA_VERSION + 1), + ("index_schema_version", INDEX_SCHEMA_VERSION + 1), + ("generation_id", uuid1().hex), + ("generation_id", str(uuid4())), + ("state", "running"), + ("completed_at", "2026-07-29T00:01:00"), + ) + for field, value in invalid_values: + with self.subTest(field=field, value=value): + payload = completed_manifest() + payload[field] = value + with self.assertRaises(ValidationError): + self.validate(payload) + + def test_requires_one_consistent_completed_media_without_failures(self): + mutations = [] + + second_input = deepcopy(completed_manifest()["inputs"]["episode-1"]) + mutations.append( + lambda payload: payload["inputs"].update( + {"episode-2": second_input} + ) + ) + mutations.append( + lambda payload: payload.update( + {"completed_videos": ["different-media"]} + ) + ) + mutations.append( + lambda payload: payload["videos"]["episode-1"].update( + {"state": "failed"} + ) + ) + mutations.append( + lambda payload: payload.update({"failed_videos": ["episode-1"]}) + ) + mutations.append( + lambda payload: payload.update( + {"interrupted_videos": ["episode-1"]} + ) + ) + + for mutate in mutations: + payload = completed_manifest() + mutate(payload) + with self.assertRaises(ValidationError): + self.validate(payload) + + def test_record_counts_exactly_match_modalities_and_sizes_are_nonnegative(self): + invalid_counts = ( + {"scene": 4}, + {"scene": 4, "dialogue": 2, "actor": 1}, + {"scene": -1, "dialogue": 2}, + ) + for record_counts in invalid_counts: + with self.subTest(record_counts=record_counts): + payload = completed_manifest() + payload["record_counts"] = record_counts + with self.assertRaises(ValidationError): + self.validate(payload) + + unknown_size = completed_manifest() + unknown_size["store_size_bytes_at_commit"] = None + self.assertIsNone( + self.validate(unknown_size).store_size_bytes_at_commit + ) + + for field in ("processed_frames", "store_size_bytes_at_commit"): + with self.subTest(field=field): + payload = completed_manifest() + payload[field] = -1 + with self.assertRaises(ValidationError): + self.validate(payload) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_index_state.py b/tests/test_index_state.py deleted file mode 100644 index 9395802..0000000 --- a/tests/test_index_state.py +++ /dev/null @@ -1,95 +0,0 @@ -import json -import tempfile -import unittest -from pathlib import Path - -from vidxp.index_state import ( - IndexNotReadyError, - fingerprint_file, - read_index_status, - require_ready_index, - write_index_status, -) - - -class IndexStateTests(unittest.TestCase): - def test_missing_status_is_not_ready(self): - with tempfile.TemporaryDirectory() as directory: - with self.assertRaisesRegex( - IndexNotReadyError, - "Index a video before searching", - ): - require_ready_index(directory) - - def test_status_round_trip_and_readiness_guard(self): - with tempfile.TemporaryDirectory() as directory: - write_index_status( - state="indexing", - stage="scene_indexing", - message="Indexing video scenes.", - current=5, - total=10, - index_directory=directory, - ) - - status = read_index_status(directory) - self.assertEqual(status["state"], "indexing") - self.assertEqual(status["current"], 5) - with self.assertRaises(IndexNotReadyError): - require_ready_index(directory) - - write_index_status( - state="ready", - stage="complete", - message="Video indexing completed successfully.", - summary={"scene_frames": 10}, - index_directory=directory, - ) - self.assertEqual(require_ready_index(directory)["state"], "ready") - - def test_unreadable_status_is_failed(self): - with tempfile.TemporaryDirectory() as directory: - status_path = Path(directory) / "index_status.json" - status_path.write_text("{not-json", encoding="utf-8") - - status = read_index_status(directory) - self.assertEqual(status["state"], "failed") - with self.assertRaises(IndexNotReadyError): - require_ready_index(directory) - - def test_file_fingerprint_is_stable(self): - with tempfile.TemporaryDirectory() as directory: - video_path = Path(directory) / "sample.mp4" - video_path.write_bytes(b"vidxp-test-video") - - first = fingerprint_file(video_path) - second = fingerprint_file(video_path) - - self.assertEqual(first["sha256"], second["sha256"]) - self.assertEqual(first["size"], len(b"vidxp-test-video")) - self.assertEqual(first["path"], str(video_path.resolve())) - - def test_status_write_replaces_previous_payload(self): - with tempfile.TemporaryDirectory() as directory: - write_index_status( - state="failed", - stage="audio", - message="Failed.", - error="example", - index_directory=directory, - ) - write_index_status( - state="ready", - stage="complete", - message="Ready.", - index_directory=directory, - ) - - status_path = Path(directory) / "index_status.json" - payload = json.loads(status_path.read_text(encoding="utf-8")) - self.assertEqual(payload["state"], "ready") - self.assertNotIn("error", payload) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_index_worker.py b/tests/test_index_worker.py deleted file mode 100644 index aeb1a84..0000000 --- a/tests/test_index_worker.py +++ /dev/null @@ -1,150 +0,0 @@ -import unittest -from pathlib import Path -from unittest.mock import Mock, patch - -from vidxp import index_worker -from vidxp.index_state import IndexingInProgressError - - -class FakeProcess: - def __init__(self, **options): - self.options = options - self.alive = False - - def start(self): - self.alive = True - - def is_alive(self): - return self.alive - - -class FakeEvent: - def __init__(self): - self.set_called = False - - def set(self): - self.set_called = True - - def is_set(self): - return self.set_called - - -class FakeContext: - def __init__(self): - self.process = None - self.event = FakeEvent() - - def Event(self): - return self.event - - def Process(self, **options): - self.process = FakeProcess(**options) - return self.process - - -class IndexWorkerTests(unittest.TestCase): - def setUp(self): - index_worker._process = None - index_worker._cancel_event = None - self.service = Mock() - self.service.index_directory = Path("selected-index") - self.service.device = "cuda" - self.service.indexing_in_progress.return_value = False - - def tearDown(self): - index_worker._process = None - index_worker._cancel_event = None - - def test_worker_starts_indexing_in_a_separate_process(self): - context = FakeContext() - - with patch.object(index_worker, "get_context", return_value=context): - index_worker.start_indexing( - "video.mp4", - "source.mp4", - self.service, - modalities=("scene",), - ) - - self.assertTrue(context.process.is_alive()) - self.assertEqual(context.process.options["name"], "vidxp-indexer") - self.assertTrue(context.process.options["daemon"]) - self.assertEqual( - context.process.options["args"], - ( - "video.mp4", - "source.mp4", - context.event, - "selected-index", - "cuda", - ("scene",), - ), - ) - self.assertIs(context.process.options["target"], index_worker._run_indexing) - - def test_worker_process_reconstructs_the_selected_service(self): - service = Mock() - with patch.object( - index_worker, - "VidXPService", - return_value=service, - ) as service_type: - index_worker._run_indexing( - "video.mp4", - "source.mp4", - FakeEvent(), - "selected-index", - "cuda", - ("scene",), - ) - - service_type.assert_called_once_with("selected-index", device="cuda") - service.create_index.assert_called_once() - self.assertEqual( - service.create_index.call_args.kwargs["source_name"], - "source.mp4", - ) - self.assertEqual( - service.create_index.call_args.kwargs["modalities"], - ("scene",), - ) - - def test_worker_rejects_a_second_indexing_run(self): - context = FakeContext() - - with patch.object(index_worker, "get_context", return_value=context): - index_worker.start_indexing( - "video.mp4", - "source.mp4", - self.service, - modalities=("scene",), - ) - with self.assertRaises(IndexingInProgressError): - index_worker.start_indexing( - "video.mp4", - "source.mp4", - self.service, - modalities=("scene",), - ) - - def test_existing_service_run_remains_visible(self): - self.service.indexing_in_progress.return_value = True - - self.assertTrue(index_worker.indexing_in_progress(self.service)) - - def test_cancellation_requests_are_cooperative(self): - context = FakeContext() - with patch.object(index_worker, "get_context", return_value=context): - index_worker.start_indexing( - "video.mp4", - "source.mp4", - self.service, - modalities=("scene",), - ) - - self.assertTrue(index_worker.cancel_indexing()) - self.assertTrue(context.event.set_called) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_indexing.py b/tests/test_indexing.py index b8ebb7d..91766c1 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -1,137 +1,121 @@ -import sys -import types import unittest +from contextlib import nullcontext +import sys +from types import SimpleNamespace from unittest.mock import Mock, patch import numpy as np -from vidxp.capabilities import visual as indexing_visual_module +from vidxp.capabilities.actor.indexing import ( + _actor_cluster_id, + _actor_cluster_records, + _actor_records, +) +from vidxp.capabilities.contracts import ( + CapabilityDefinition, + CapabilityExecutor, + CapabilityPlugin, +) from vidxp.capabilities.dialogue.indexing import ( build_dialogue_phrases, index_dialogue, + transcribe_video, +) +from vidxp.capabilities.scene.indexing import ( + encode_scene_batch, + scene_records, + scene_sampling, +) +from vidxp.capabilities.scene.models import SceneModel +from vidxp.capabilities.registry import ( + CapabilityRegistry, + create_capability_registry, ) from vidxp.capabilities.visual import index_visuals -from vidxp.core.contracts import CancellationToken, IndexConfig, VideoSource +from vidxp.core.contracts import ( + CancellationToken, + IndexConfig, + VideoSource, +) from vidxp.core.video import FrameSample, VideoInfo +from vidxp.runtime import ModelRuntime +from vidxp.settings import VidXPSettings class CapturingStorage: def __init__(self): self.calls = [] - self.deleted_actor_clusters = [] def upsert(self, modality, records, **options): - self.calls.append((modality, list(records), options)) + records = list(records) + self.calls.append((modality, records, options)) return len(records) - def delete_records(self, modality, *, video_id, filters): - self.deleted_actor_clusters.append( - (modality, video_id, filters["cluster_id"]) - ) - class FakeEncoder: def __init__(self): self.batches = [] - def encode(self, texts, **_): + def encode_document(self, texts, **_): self.batches.append(list(texts)) - return np.asarray([[float(index), 1.0] for index, _ in enumerate(texts)]) - - -class FakeClipModel: - def __init__(self): - self.batch_sizes = [] - - def encode_image(self, images): - import torch - - self.batch_sizes.append(images.shape[0]) - return torch.ones((images.shape[0], 2), dtype=torch.float32) - - -class IndexingTests(unittest.TestCase): - def test_shared_visual_stream_accepts_a_registered_processor(self): - frame = np.zeros((2, 2, 3), dtype=np.uint8) - info = VideoInfo( - fps=10.0, - frame_count=1, - duration=0.1, - width=2, - height=2, + return np.asarray( + [[float(index), 1.0] for index, _ in enumerate(texts)] ) - processor = Mock() - processor.batch_size.return_value = 1 - processor.prepare.return_value = object() - processor.finalize.return_value = ({"ocr_frames": 1}, 1) - def stream(*_, **options): - options["stats"].frames_advanced = 1 - options["stats"].frames_materialized = 1 - return iter([[FrameSample(0, 0.0, frame)]]) - config = IndexConfig( - video_id="video-1", - enabled_modalities=("ocr",), - collection_names={"ocr": "ocr"}, - ) - with ( - patch( - "vidxp.capabilities.registry.get_capability", - return_value=Mock(index_processor=processor), - ), - patch( - "vidxp.capabilities.visual.probe_video", - return_value=info, - ), - patch( - "vidxp.capabilities.visual.iter_frame_batches", - side_effect=stream, - ), - patch( - "vidxp.capabilities.visual._rgb_samples", - side_effect=lambda samples: samples, - ), - ): - result = index_visuals( - VideoSource(path="unused.mp4"), - config=config, - storage=CapturingStorage(), - cancellation=CancellationToken(), +class IndexingTests(unittest.TestCase): + def runtime(self) -> ModelRuntime: + return ModelRuntime( + VidXPSettings( + repository_root="unused", + runtime_backend="cpu", ) + ) - processor.prepare.assert_called_once() - processor.process.assert_called_once() - processor.finalize.assert_called_once() - self.assertEqual(result.summary["ocr_frames"], 1) - self.assertEqual(result.summary["frame_operations"], 1) - - def test_timestamped_words_and_segments_keep_real_intervals(self): + def test_word_timestamps_are_grouped_without_interpolation(self): phrases = build_dialogue_phrases( [ { + "text": "ignored aggregate text", + "start": 0.0, + "end": 4.0, "words": [ - {"word": "one", "start": 0.0, "end": 0.4}, - {"word": "two", "start": 0.5, "end": 0.9}, - {"word": "three", "start": 1.0, "end": 1.4}, - ] - }, - {"text": "released ASR span", "start": 2.0, "end": 4.0}, + {"word": "one", "start": 0.1, "end": 0.5}, + {"word": "two", "start": 0.6, "end": 1.0}, + {"word": "three", "start": 1.2, "end": 1.8}, + ], + } ], words_per_phrase=2, ) self.assertEqual( [(item.text, item.start, item.end) for item in phrases], - [ - ("one two", 0.0, 0.9), - ("three", 1.0, 1.4), - ("released ASR", 2.0, 2.0 + 4.0 / 3.0), - ("span", 2.0 + 4.0 / 3.0, 4.0), - ], + [("one two", 0.1, 1.0), ("three", 1.2, 1.8)], + ) + + def test_siglip2_uses_transformers_five_pooled_image_output(self): + import torch + + processor = Mock( + return_value={"pixel_values": torch.ones((1, 3, 2, 2))} + ) + model = Mock() + model.get_image_features.return_value = SimpleNamespace( + pooler_output=torch.tensor([[3.0, 4.0]]) ) + provider = SceneModel(model=model, processor=processor, device="cpu") + sample = FrameSample( + frame_index=0, + timestamp=0.0, + frame=np.zeros((2, 2, 3), dtype=np.uint8), + ) + + vectors = encode_scene_batch([sample], provider) + + self.assertEqual(vectors, [[0.6000000238418579, 0.800000011920929]]) - def test_segment_text_is_rechunked_with_interpolated_timestamps(self): + def test_segment_text_uses_interpolated_timestamps_as_fallback(self): phrases = build_dialogue_phrases( [ { @@ -151,7 +135,7 @@ def test_segment_text_is_rechunked_with_interpolated_timestamps(self): ], ) - def test_supplied_transcript_is_batched_without_video_or_whisper(self): + def test_transcript_indexing_batches_without_transcription(self): config = IndexConfig( dataset="hirest", split="test", @@ -179,7 +163,7 @@ def test_supplied_transcript_is_batched_without_video_or_whisper(self): ), patch( "vidxp.capabilities.dialogue.indexing.transcribe_video", - side_effect=AssertionError("video/Whisper path was used"), + side_effect=AssertionError("transcription was used"), ), ): stats = index_dialogue( @@ -187,270 +171,331 @@ def test_supplied_transcript_is_batched_without_video_or_whisper(self): config=config, storage=storage, cancellation=CancellationToken(), + runtime=self.runtime(), ) self.assertEqual([len(batch) for batch in encoder.batches], [2, 1]) self.assertEqual(stats["dialogue_phrases"], 3) - records = [ - record - for _, group, _ in storage.calls - for record in group - ] - self.assertEqual(len({record.source_id for record in records}), 3) - self.assertEqual( - set(records[0].metadata), + + def test_silent_video_skips_dialogue_before_loading_whisper(self): + events = [] + fake_av = SimpleNamespace( + open=Mock( + return_value=nullcontext( + SimpleNamespace( + streams=SimpleNamespace(audio=()), + ) + ) + ) + ) + pipeline = Mock(side_effect=AssertionError("whisper was loaded")) + fake_whisper = SimpleNamespace(BatchedInferencePipeline=pipeline) + + with patch.dict( + sys.modules, { - "dataset", - "split", - "run_id", - "video_id", - "modality", - "source_id", - "phrase_id", - "text", - "start", - "end", + "av": fake_av, + "faster_whisper": fake_whisper, }, - ) + ): + segments, language = transcribe_video( + "silent.mp4", + config=IndexConfig( + video_id="video-1", + enabled_modalities=("dialogue",), + ), + cancellation=CancellationToken(), + runtime=self.runtime(), + progress=events.append, + ) - def test_scene_model_and_storage_writes_are_batched_with_full_metadata(self): - import torch + self.assertEqual(segments, []) + self.assertIsNone(language) + self.assertEqual(events[-1]["stage"], "dialogue_skipped") + pipeline.assert_not_called() + def test_visual_stream_uses_registered_processor_without_name_switches(self): + processor = Mock() + processor.batch_size.return_value = 1 + processor.prepare.return_value = object() + processor.finalize.return_value = ({"ocr_frames": 1}, 1) + definition = CapabilityDefinition( + name="ocr", + description="OCR.", + extra="ocr", + collection_name="ocr", + index_stage="visual_indexing", + execution_group="visual", + ) + registry = CapabilityRegistry( + ( + CapabilityPlugin( + definition=definition, + executor_factory=lambda: CapabilityExecutor( + indexer=index_visuals, + index_processor=processor, + ), + ), + ) + ) config = IndexConfig( - dataset="didemo", - split="test", - run_id="stride-2", video_id="video-1", - enabled_modalities=("scene",), - frame_stride=2, - capability_options={"scene": {"batch_size": 2}}, + enabled_modalities=("ocr",), + collection_names={"ocr": "ocr"}, ) - source = VideoSource(video_id="video-1", path="unused.mp4") - storage = CapturingStorage() - model = FakeClipModel() + info = VideoInfo(10.0, 1, 0.1, 2, 2) frame = np.zeros((2, 2, 3), dtype=np.uint8) - batches = [ - [ - FrameSample(0, 0.0, frame), - FrameSample(2, 0.2, frame), - ], - [FrameSample(4, 0.4, frame)], - ] - info = VideoInfo( - fps=10.0, - frame_count=5, - duration=0.5, - width=2, - height=2, - ) + + def stream(*_, **options): + options["stats"].frames_advanced = 1 + options["stats"].frames_materialized = 1 + return iter([[FrameSample(0, 0.0, frame)]]) with ( - patch("vidxp.capabilities.visual.probe_video", return_value=info), patch( - "vidxp.capabilities.visual.iter_frame_batches", - return_value=iter(batches), + "vidxp.capabilities.visual.probe_video", + return_value=info, ), patch( - "vidxp.capabilities.scene.indexing.get_clip_model", - return_value=( - model, - lambda _: torch.ones((3, 2, 2), dtype=torch.float32), - ), + "vidxp.capabilities.visual.iter_frame_batches", + side_effect=stream, ), ): - stats = index_visuals( - source, + result = index_visuals( + VideoSource(video_id="video-1", path="unused.mp4"), config=config, - storage=storage, + storage=CapturingStorage(), cancellation=CancellationToken(), - modalities=("scene",), - ).summary - - self.assertEqual(model.batch_sizes, [2, 1]) - self.assertEqual(stats["scene_frames"], 3) - records = [ - record - for _, group, _ in storage.calls - for record in group - ] - self.assertEqual( - [record.metadata["frame_index"] for record in records], - [0, 2, 4], - ) - self.assertEqual(records[-1].metadata["end"], 0.5) - self.assertEqual(records[0].metadata["fps"], 10.0) - self.assertEqual(records[0].metadata["duration"], 0.5) + registry=registry, + runtime=self.runtime(), + ) - def test_actor_only_records_have_stable_detection_metadata(self): + self.assertEqual(result.summary["ocr_frames"], 1) + processor.prepare.assert_called_once() + processor.process.assert_called_once() + + def test_scene_sampling_uses_target_fps_and_media_timestamps(self): config = IndexConfig( - dataset="sample", - split="test", - run_id="actors", video_id="video-1", - enabled_modalities=("actor",), - capability_options={"actor": {"minimum_detections": 2}}, - storage_batch_size=2, + enabled_modalities=("scene",), + capability_options={"scene": {"sample_fps": 1.0}}, ) - source = VideoSource(video_id="video-1", path="unused.mp4") - storage = CapturingStorage() - info = VideoInfo( - fps=10.0, - frame_count=4, - duration=0.4, - width=2, - height=2, + + near_below = scene_sampling( + config, + VideoInfo(1.49, 149, 100.0, 2, 2), ) - frame = np.zeros((2, 2, 3), dtype=np.uint8) - batches = [[ - FrameSample(0, 0.0, frame), - FrameSample(2, 0.2, frame), - ]] - fake_faces = types.SimpleNamespace( - face_locations=lambda _: [(1, 2, 3, 0)], - face_encodings=lambda *_args, **_kwargs: [ - np.asarray([1.0, 0.0]) - ], - face_distance=lambda known, _: np.asarray( - [0.0 for _ in known] - ), + near_above = scene_sampling( + config, + VideoInfo(1.5, 150, 100.0, 2, 2), + ) + below_indices = [ + index + for index in range(149) + if near_below.includes(index, index / 1.49) + ] + above_indices = [ + index + for index in range(150) + if near_above.includes(index, index / 1.5) + ] + + self.assertEqual(below_indices[:4], [0, 2, 3, 5]) + self.assertEqual(above_indices[:4], [0, 2, 3, 5]) + self.assertEqual(len(below_indices), 100) + self.assertEqual(len(above_indices), 100) + + low_fps = scene_sampling( + config, + VideoInfo(0.5, 5, 10.0, 2, 2), ) - with ( - patch("vidxp.capabilities.visual.probe_video", return_value=info), - patch( - "vidxp.capabilities.visual.iter_frame_batches", - return_value=iter(batches), - ), - patch.dict(sys.modules, {"face_recognition": fake_faces}), - ): - stats = index_visuals( - source, - config=config, - storage=storage, - cancellation=CancellationToken(), - modalities=("actor",), - ).summary - - self.assertEqual(stats["actor_frames"], 2) - self.assertEqual(stats["actor_detections"], 2) - modality, records, options = storage.calls[0] - self.assertEqual(modality, "actor") - self.assertEqual(options["batch_size"], 2) self.assertEqual( - [record.metadata["detection_id"] for record in records], [ - "d000000000000-0000", - "d000000000002-0000", + index + for index in range(5) + if low_fps.includes(index, index / 0.5) ], + [0, 1, 2, 3, 4], ) - self.assertEqual( - set(records[0].metadata), - { - "dataset", - "split", - "run_id", - "video_id", - "modality", - "source_id", - "detection_id", - "cluster_id", - "frame_index", - "timestamp", - "bbox_top", - "bbox_right", - "bbox_bottom", - "bbox_left", - }, + + def test_scene_record_duration_uses_scene_cadence_not_actor_stride(self): + config = IndexConfig( + video_id="video-1", + enabled_modalities=("scene",), + frame_stride=3, + capability_options={"scene": {"sample_fps": 2.0}}, ) + info = VideoInfo(30.0, 300, 10.0, 2, 2) - def test_scene_and_actor_share_one_probe_and_frame_stream(self): - import torch + record = scene_records( + [FrameSample(15, 0.5, object())], + [[0.1, 0.2]], + info, + config, + )[0] + + self.assertEqual(record.metadata["start"], 0.5) + self.assertEqual(record.metadata["end"], 1.0) + def test_scene_records_end_at_next_selected_sample_without_gaps(self): config = IndexConfig( - dataset="sample", - split="test", - run_id="shared", video_id="video-1", - enabled_modalities=("scene", "actor"), - frame_stride=2, - capability_options={ - "scene": {"batch_size": 2}, - "actor": { - "batch_size": 1, - "minimum_detections": 1, - }, - }, + enabled_modalities=("scene",), + capability_options={"scene": {"sample_fps": 1.0}}, ) - source = VideoSource(video_id="video-1", path="unused.mp4") - storage = CapturingStorage() - model = FakeClipModel() - frame = np.zeros((2, 2, 3), dtype=np.uint8) - batches = [ + info = VideoInfo(1.49, 5, 5 / 1.49, 2, 2) + + records = scene_records( [ - FrameSample(0, 0.0, frame), - FrameSample(2, 0.2, frame), + FrameSample(0, 0.0, object()), + FrameSample(2, 2 / 1.49, object()), + FrameSample(3, 3 / 1.49, object()), ], - [FrameSample(4, 0.4, frame)], - ] - info = VideoInfo( - fps=10.0, - frame_count=5, - duration=0.5, - width=2, - height=2, + [[0.1], [0.2], [0.3]], + info, + config, ) - frame_stream = Mock() + self.assertEqual(records[0].metadata["end"], 2 / 1.49) + self.assertEqual(records[1].metadata["end"], 3 / 1.49) + self.assertEqual(records[2].metadata["end"], info.duration) + + def test_scene_and_actor_share_decode_but_keep_independent_cadences(self): + registry = create_capability_registry() + scene = registry.executor("scene").index_processor + actor = registry.executor("actor").index_processor + scene.prepare = Mock(return_value=object()) + actor.prepare = Mock(return_value=object()) + scene.process = Mock() + actor.process = Mock() + scene.batch_size = Mock(return_value=2) + actor.batch_size = Mock(return_value=1) + scene.finalize = Mock(return_value=({"scene_frames": 1}, 1)) + actor.finalize = Mock( + return_value=( + {"actor_frames": 2, "actor_detections": 0, "actor_clusters": 0}, + 2, + ) + ) + info = VideoInfo(10.0, 2, 0.2, 2, 2) + frame = np.zeros((2, 2, 3), dtype=np.uint8) def stream(*_, **options): - options["stats"].frames_advanced = 5 - options["stats"].frames_materialized = 3 - return iter(batches) - - frame_stream.side_effect = stream - - def consume_actor(samples, *, state, **_): - state.processed_frames += len(samples) + options["stats"].frames_advanced = 2 + options["stats"].frames_materialized = 2 + return iter( + [[FrameSample(0, 0.0, frame), FrameSample(1, 0.1, frame)]] + ) + frame_stream = Mock(side_effect=stream) + config = IndexConfig( + video_id="video-1", + enabled_modalities=("scene", "actor"), + ) with ( patch( "vidxp.capabilities.visual.probe_video", return_value=info, - ) as probe, + ), patch( "vidxp.capabilities.visual.iter_frame_batches", frame_stream, ), - patch( - "vidxp.capabilities.scene.indexing.get_clip_model", - return_value=( - model, - lambda _: torch.ones((3, 2, 2), dtype=torch.float32), - ), - ), - patch( - "vidxp.capabilities.actor.indexing.process_actor_samples", - side_effect=consume_actor, - ) as actor_consumer, - patch( - "vidxp.capabilities.visual._rgb_samples", - wraps=indexing_visual_module._rgb_samples, - ) as rgb_conversion, ): result = index_visuals( - source, + VideoSource(video_id="video-1", path="unused.mp4"), config=config, - storage=storage, + storage=CapturingStorage(), cancellation=CancellationToken(), + registry=registry, + runtime=self.runtime(), ) - probe.assert_called_once_with("unused.mp4") frame_stream.assert_called_once() - self.assertEqual(frame_stream.call_args.kwargs["batch_size"], 2) - self.assertEqual(model.batch_sizes, [2, 1]) - self.assertEqual(actor_consumer.call_count, 2) - self.assertEqual(rgb_conversion.call_count, 2) - self.assertEqual(result.summary["source_frames_advanced"], 5) - self.assertEqual(result.summary["sampled_frames"], 3) - self.assertEqual(result.summary["frame_operations"], 6) + samplings = frame_stream.call_args.kwargs["samplings"] + self.assertEqual(samplings[0].target_fps, 1.0) + self.assertEqual(samplings[1].frame_stride, 1) + self.assertEqual( + [sample.frame_index for sample in scene.process.call_args.args[0]], + [0], + ) + self.assertEqual( + [sample.frame_index for sample in actor.process.call_args.args[0]], + [0, 1], + ) + self.assertEqual(result.summary["processed_frames"], 2) + self.assertEqual(result.summary["scene_frames"], 1) + self.assertEqual(result.summary["actor_frames"], 2) + + def test_actor_records_preserve_stable_detection_metadata(self): + config = IndexConfig( + dataset="sample", + split="test", + run_id="actors", + video_id="video-1", + generation_id="generation-1", + enabled_modalities=("actor",), + ) + cluster_id = _actor_cluster_id(config, 1) + records = _actor_records( + [ + { + "detection_id": "d000000000000-0000", + "cluster_id": cluster_id, + "frame_index": 0, + "timestamp": 0.0, + "bbox": (1, 2, 3, 0), + } + ], + config, + ) + + self.assertEqual(records[0].metadata["cluster_id"], cluster_id) + self.assertEqual( + cluster_id, + "generation-1:actors:video-1:actor-cluster:1", + ) + self.assertEqual(records[0].metadata["bbox_top"], 1) + + def test_actor_cluster_identity_is_unique_by_media_and_generation(self): + def config(video_id, generation_id): + return IndexConfig( + run_id="actors", + video_id=video_id, + generation_id=generation_id, + enabled_modalities=("actor",), + ) + + identities = { + _actor_cluster_id(config("video-1", "generation-1"), 1), + _actor_cluster_id(config("video-2", "generation-1"), 1), + _actor_cluster_id(config("video-1", "generation-2"), 1), + } + + self.assertEqual(len(identities), 3) + + def test_actor_cluster_summaries_are_materialized_for_bounded_paging(self): + config = IndexConfig( + run_id="actors", + video_id="video-1", + generation_id="generation-1", + enabled_modalities=("actor",), + ) + + records = _actor_cluster_records( + {"cluster-1": 3}, + {"cluster-1": (1.25, 4.5)}, + config, + ) + + self.assertEqual(len(records), 1) + self.assertEqual(records[0].metadata["record_kind"], "cluster_summary") + self.assertEqual( + records[0].metadata["summary_cluster_id"], + "cluster-1", + ) + self.assertEqual(records[0].metadata["detection_count"], 3) + self.assertEqual(records[0].metadata["first_timestamp"], 1.25) + self.assertEqual(records[0].metadata["last_timestamp"], 4.5) if __name__ == "__main__": diff --git a/tests/test_job_contracts.py b/tests/test_job_contracts.py new file mode 100644 index 0000000..817f05f --- /dev/null +++ b/tests/test_job_contracts.py @@ -0,0 +1,308 @@ +import json +import unittest +from datetime import datetime, timezone +from pathlib import Path +from unittest.mock import Mock + +from pydantic import ValidationError + +from vidxp.application_models import ( + ActorOverlayJobRequest, + ApplicationError, + CreateActorOverlayCommand, + CreateIndexCommand, + Job, + JobKind, + JobProgress, + JobQueue, + JobState, + InvalidRequestError, + IndexSnapshotReference, + ListJobsCommand, + QueryJobRequest, + QueryVideoCommand, + SearchCommand, + SearchJobRequest, +) +from vidxp.core.storage import BUNDLED_CHROMA_SERVER_URL +from vidxp.job_service import JobService +from vidxp.ports import InvalidJobBackendRequestError +from vidxp.execution import ExecutionContext +from vidxp.composition import _server_chroma_url +from vidxp.settings import ApplicationMode, VidXPSettings +from vidxp.workflow_runtime import ( + BUNDLED_POSTGRES_DATABASE_URL, + workflow_database_url, +) + + +MEDIA_ID = "123456781234423481234567890abcde" +JOB_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" +SNAPSHOT_SHA256 = "a" * 64 + + +class JobContractTests(unittest.TestCase): + def test_progress_is_typed_and_position_is_bounded(self): + progress = JobProgress( + stage="frames", + message="Indexing frames.", + current=2, + total=3, + updated_at=datetime.now(timezone.utc), + ) + + self.assertEqual(progress.schema_version, 1) + with self.assertRaises(ValidationError): + JobProgress( + stage="frames", + message="Invalid.", + current=4, + total=3, + updated_at=datetime.now(timezone.utc), + ) + + def test_public_job_contract_has_no_path_or_storage_fields(self): + job = Job( + job_id=JOB_ID, + kind=JobKind.index, + state=JobState.queued, + queue=JobQueue.cpu, + ) + schema = json.dumps(Job.model_json_schema()) + self.assertEqual(job.schema_version, 2) + self.assertNotIn("storage_key", schema) + self.assertNotIn('"path"', schema) + self.assertNotIn("model_cache", schema) + + def test_job_service_routes_model_work_without_reimplementing_it(self): + backend = Mock() + backend.submit.return_value = Job( + job_id=JOB_ID, + kind=JobKind.index, + state=JobState.queued, + queue=JobQueue.cpu, + ) + service = JobService( + settings=VidXPSettings( + repository_root=Path("repository"), + runtime_backend="cpu", + ), + backend=backend, + ) + + job = service.submit_index( + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + ) + ) + + self.assertEqual(job.job_id, JOB_ID) + request = backend.submit.call_args.args[0] + self.assertEqual(request.kind, JobKind.index) + self.assertEqual(request.command.media_id, MEDIA_ID) + self.assertEqual( + backend.submit.call_args.kwargs["queue"], + JobQueue.cpu, + ) + + def test_job_list_cursor_is_bounded(self): + with self.assertRaises(ValidationError): + ListJobsCommand(cursor="x" * 513) + + def test_search_uses_the_same_model_worker_queue(self): + backend = Mock() + planner = Mock() + command = SearchCommand( + modalities=("scene",), + query="taxi", + top_k=2, + ) + planner.plan_search.return_value = SearchJobRequest( + command=command, + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + backend.submit.return_value = Job( + job_id=JOB_ID, + kind=JobKind.search, + state=JobState.queued, + queue=JobQueue.gpu, + ) + service = JobService( + settings=VidXPSettings( + repository_root=Path("repository"), + runtime_backend="cuda:0", + ), + backend=backend, + read_planner=planner, + ) + + service.submit_search(command) + + request = backend.submit.call_args.args[0] + self.assertEqual(request.kind, JobKind.search) + self.assertEqual(request.snapshot.snapshot_id, SNAPSHOT_ID) + planner.plan_search.assert_called_once_with(command) + self.assertEqual( + backend.submit.call_args.kwargs["queue"], + JobQueue.gpu, + ) + + def test_query_uses_the_same_pinned_model_worker_boundary(self): + backend = Mock() + planner = Mock() + command = QueryVideoCommand(question="What happens next?") + planner.plan_query.return_value = QueryJobRequest( + command=command, + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + backend.submit.return_value = Job( + job_id=JOB_ID, + kind=JobKind.query, + state=JobState.queued, + queue=JobQueue.gpu, + ) + service = JobService( + settings=VidXPSettings( + repository_root=Path("repository"), + runtime_backend="cuda:0", + ), + backend=backend, + read_planner=planner, + ) + + service.submit_query(command) + + request = backend.submit.call_args.args[0] + self.assertEqual(request.kind, JobKind.query) + self.assertEqual(request.snapshot.snapshot_id, SNAPSHOT_ID) + planner.plan_query.assert_called_once_with(command) + self.assertEqual( + backend.submit.call_args.kwargs["queue"], + JobQueue.gpu, + ) + + def test_actor_job_retains_pinned_snapshot_identity(self): + backend = Mock() + planner = Mock() + command = CreateActorOverlayCommand(cluster_id="actor-1") + planner.plan_actor_overlay.return_value = ActorOverlayJobRequest( + command=command, + snapshot=IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ), + ) + backend.submit.return_value = Job( + job_id=JOB_ID, + kind=JobKind.actor_overlay, + state=JobState.queued, + queue=JobQueue.cpu, + ) + service = JobService( + settings=VidXPSettings( + repository_root=Path("repository"), + runtime_backend="cpu", + ), + backend=backend, + read_planner=planner, + ) + + service.submit_actor_overlay(command) + + request = backend.submit.call_args.args[0] + self.assertEqual(request.snapshot.snapshot_id, SNAPSHOT_ID) + planner.plan_actor_overlay.assert_called_once_with(command) + + def test_search_and_actor_identifiers_are_bounded(self): + command = SearchCommand( + modalities=(" scene ",), + query=" a chef prepares pizza ", + ) + self.assertEqual(command.modalities, ("scene",)) + self.assertEqual(command.query, "a chef prepares pizza") + + for values in ( + {"modalities": ["scene/video"], "query": "taxi"}, + {"modalities": ["scene"], "query": "x" * 4097}, + {"modalities": ["scene"], "query": " "}, + ): + with self.assertRaises(ValidationError): + SearchCommand(**values) + + with self.assertRaises(ValidationError): + CreateActorOverlayCommand(cluster_id="../../video") + encoded_cluster = CreateActorOverlayCommand( + cluster_id=( + "generation:run%20name:media:actor-cluster:1" + ) + ) + self.assertIn("%20", encoded_cluster.cluster_id) + + def test_canonical_dbos_job_id_has_a_hex_operation_identity(self): + execution = ExecutionContext( + job_id="22345678-1234-4234-8123-4567890abcde" + ) + + self.assertEqual(execution.operation_id, JOB_ID) + + def test_local_storage_uses_repository_services(self): + settings = VidXPSettings( + repository_root=Path("repository"), + ) + + self.assertEqual( + workflow_database_url(settings), + ( + "sqlite:///" + f"{settings.layout.workflow_database.resolve().as_posix()}" + ), + ) + self.assertIsNone(_server_chroma_url(settings)) + + def test_server_uses_bundled_storage_services(self): + settings = VidXPSettings( + mode=ApplicationMode.server, + runtime_backend="cpu", + ) + + self.assertEqual( + workflow_database_url(settings), + BUNDLED_POSTGRES_DATABASE_URL, + ) + self.assertEqual( + _server_chroma_url(settings), + BUNDLED_CHROMA_SERVER_URL, + ) + + def test_job_backend_errors_are_normalized_for_every_adapter(self): + backend = Mock() + service = JobService( + settings=VidXPSettings( + repository_root=Path("repository"), + runtime_backend="cpu", + ), + backend=backend, + ) + backend.get.side_effect = InvalidJobBackendRequestError( + "invalid workflow id" + ) + + with self.assertRaises(InvalidRequestError): + service.get("invalid") + + backend.get.side_effect = RuntimeError("database unavailable") + with self.assertRaises(ApplicationError) as raised: + service.get(JOB_ID) + self.assertEqual(raised.exception.code, "job_backend_unavailable") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_local_snapshots.py b/tests/test_local_snapshots.py new file mode 100644 index 0000000..091060c --- /dev/null +++ b/tests/test_local_snapshots.py @@ -0,0 +1,798 @@ +import json +import unittest +from dataclasses import replace +from datetime import datetime, timezone +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch +from unittest.mock import MagicMock, Mock + +from vidxp.core.contracts import ( + CancellationToken, + INDEX_SCHEMA_VERSION, + MANIFEST_SCHEMA_VERSION, + IndexConfig, + IndexCancelledError, + IndexSchemaError, + StorageRecord, +) +from vidxp.core.storage import IndexStorage +from vidxp.core.manifest import MANIFEST_FILE, write_json_atomic +from vidxp.infrastructure.local_snapshots import LocalSnapshotRepository +from vidxp.infrastructure.local_index import LocalIndexBackend + + +class LocalSnapshotRepositoryTests(unittest.TestCase): + def setUp(self): + self.temporary = TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.repository = LocalSnapshotRepository( + Path(self.temporary.name) / "indexes" + ) + self.config = IndexConfig.local( + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=self.repository.store, + ) + + def generation( + self, + media_id: str, + *, + input_sha: str, + store_size_bytes_at_commit: int | None = 123, + ): + generation_id = self.repository.new_generation_id() + config = replace( + self.config, + video_id=media_id, + generation_id=generation_id, + generation_directory=self.repository.generation_directory( + generation_id + ), + ) + return config, self.write_generation( + config, + media_id=media_id, + input_sha=input_sha, + store_size_bytes_at_commit=store_size_bytes_at_commit, + ) + + def write_generation( + self, + config: IndexConfig, + *, + media_id: str, + input_sha: str, + record_counts: dict[str, int] | None = None, + store_size_bytes_at_commit: int | None = 123, + ): + now = datetime.now(timezone.utc).isoformat() + manifest = { + "manifest_schema_version": MANIFEST_SCHEMA_VERSION, + "index_schema_version": INDEX_SCHEMA_VERSION, + "dataset": config.dataset, + "split": config.split, + "run_id": config.run_id, + "generation_id": config.generation_id, + "state": "complete", + "created_at": now, + "updated_at": now, + "completed_at": now, + "config_fingerprint": config.fingerprint(), + "execution_fingerprint": "e" * 64, + "configuration": config.to_dict(), + "models": {}, + "git": {}, + "environment": {}, + "inputs": { + media_id: { + "sha256": input_sha, + "checksums": {"video": input_sha}, + "size": 1, + "source_name": f"{media_id}.mp4", + "path": None, + "metadata": {}, + } + }, + "videos": { + media_id: { + "state": "complete", + "started_at": now, + "stages": {}, + "completed_at": now, + "summary": {}, + } + }, + "completed_videos": [media_id], + "failed_videos": [], + "interrupted_videos": [], + "processed_frames": 0, + "record_counts": { + modality: (record_counts or {}).get(modality, 0) + for modality in config.enabled_modalities + }, + "store_size_bytes_at_commit": store_size_bytes_at_commit, + } + write_json_atomic( + config.run_directory / MANIFEST_FILE, + manifest, + ) + return self.repository.generation_reference( + generation_id=str(config.generation_id), + media_id=media_id, + ) + + def test_unknown_store_size_round_trips_through_snapshot_metadata(self): + config, reference = self.generation( + "a", + input_sha="a" * 64, + store_size_bytes_at_commit=None, + ) + + snapshot = self.repository.publish_generation(reference, config) + + self.assertIsNone(reference.store_size_bytes_at_commit) + self.assertIsNone( + snapshot.generations["a"].store_size_bytes_at_commit + ) + + def test_add_reindex_remove_and_clear_publish_immutable_snapshots(self): + config_a1, a1 = self.generation("a", input_sha="a" * 64) + snapshot_a1 = self.repository.publish_generation(a1, config_a1) + original_a1 = ( + self.repository.snapshots / f"{snapshot_a1.snapshot_id}.json" + ).read_bytes() + + config_b1, b1 = self.generation("b", input_sha="b" * 64) + snapshot_b1 = self.repository.publish_generation(b1, config_b1) + self.assertEqual( + set(snapshot_b1.generations), + {"a", "b"}, + ) + + config_a2, a2 = self.generation("a", input_sha="c" * 64) + snapshot_a2 = self.repository.publish_generation(a2, config_a2) + self.assertEqual( + snapshot_a2.generations["a"].generation_id, + a2.generation_id, + ) + self.assertEqual( + snapshot_a2.generations["b"].generation_id, + b1.generation_id, + ) + self.assertEqual( + ( + self.repository.snapshots + / f"{snapshot_a1.snapshot_id}.json" + ).read_bytes(), + original_a1, + ) + self.assertTrue( + self.repository.generation_directory(a1.generation_id).is_dir() + ) + + self.assertTrue(self.repository.remove("a")) + self.assertEqual( + set(self.repository.read_active(required=True).generations), + {"b"}, + ) + self.assertFalse(self.repository.remove("missing")) + self.assertTrue(self.repository.clear()) + self.assertEqual( + self.repository.status()["state"], + "empty", + ) + self.assertFalse(self.repository.clear()) + + def test_pointer_failure_preserves_previous_active_snapshot(self): + config_a1, a1 = self.generation("a", input_sha="a" * 64) + previous = self.repository.publish_generation(a1, config_a1) + pointer_before = self.repository.active_pointer.read_bytes() + config_a2, a2 = self.generation("a", input_sha="b" * 64) + + real_write = write_json_atomic + + def fail_pointer(path, payload): + if path == self.repository.active_pointer: + raise OSError("injected pointer failure") + return real_write(path, payload) + + with ( + patch( + "vidxp.infrastructure.local_snapshots.write_json_atomic", + side_effect=fail_pointer, + ), + self.assertRaisesRegex(OSError, "injected"), + ): + self.repository.publish_generation(a2, config_a2) + + self.assertEqual( + self.repository.active_pointer.read_bytes(), + pointer_before, + ) + self.assertEqual( + self.repository.read_active(required=True).snapshot_id, + previous.snapshot_id, + ) + self.assertTrue( + self.repository.generation_directory(a2.generation_id).is_dir() + ) + + def test_completed_job_replay_returns_committed_generation(self): + config, reference = self.generation("a", input_sha="a" * 64) + committed = self.repository.publish_generation(reference, config) + backend = LocalIndexBackend( + Mock(), + Mock(), + self.repository.layout, + ) + storage = MagicMock() + storage.__enter__.return_value = storage + build = IndexConfig.local( + video_id="a", + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=self.repository.indexes, + ) + + with ( + patch.object( + backend, + "_open_committed_storage", + return_value=storage, + ), + patch.object(backend, "_validate_snapshot_storage"), + patch( + "vidxp.infrastructure.local_index.index_video" + ) as index_video, + ): + result = backend.create( + Path("video.mp4"), + config=build, + progress=None, + cancellation=None, + source_name="video.mp4", + source_checksum="a" * 64, + operation_id=reference.generation_id, + ) + + self.assertEqual(result["generation_id"], reference.generation_id) + self.assertEqual(result["snapshot_id"], committed.snapshot_id) + index_video.assert_not_called() + + def test_completed_generation_replay_publishes_without_rebuilding(self): + config, reference = self.generation("a", input_sha="a" * 64) + backend = LocalIndexBackend( + Mock(), + Mock(), + self.repository.layout, + ) + build = IndexConfig.local( + video_id="a", + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=self.repository.indexes, + ) + + with patch( + "vidxp.infrastructure.local_index.index_video" + ) as index_video: + result = backend.create( + Path("video.mp4"), + config=build, + progress=None, + cancellation=None, + source_name="video.mp4", + source_checksum="a" * 64, + operation_id=reference.generation_id, + ) + + self.assertEqual(result["generation_id"], reference.generation_id) + self.assertEqual( + self.repository.read_active(required=True) + .generations["a"] + .generation_id, + reference.generation_id, + ) + index_video.assert_not_called() + + def test_replay_repairs_counts_after_crash_before_generation_validation(self): + config, reference = self.generation("a", input_sha="a" * 64) + manifest_path = ( + self.repository.generation_directory(reference.generation_id) + / MANIFEST_FILE + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest.pop("record_counts") + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + backend = LocalIndexBackend( + Mock(), + Mock(), + self.repository.layout, + ) + build = IndexConfig.local( + video_id="a", + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=self.repository.indexes, + ) + storage = MagicMock() + storage.__enter__.return_value = storage + storage.records.return_value = [ + { + "generation_id": reference.generation_id, + "video_id": "a", + "modality": "scene", + } + ] + + with ( + patch( + "vidxp.infrastructure.local_index.IndexStorage", + return_value=storage, + ), + patch( + "vidxp.infrastructure.local_index.index_video" + ) as index_video, + ): + result = backend.create( + Path("video.mp4"), + config=build, + progress=None, + cancellation=None, + source_name="video.mp4", + source_checksum="a" * 64, + operation_id=reference.generation_id, + ) + + self.assertEqual(result["record_counts"], {"scene": 1}) + repaired = json.loads(manifest_path.read_text(encoding="utf-8")) + self.assertEqual(repaired["record_counts"], {"scene": 1}) + index_video.assert_not_called() + + def test_corrupt_or_missing_generation_fails_closed(self): + config, reference = self.generation("a", input_sha="a" * 64) + self.repository.publish_generation(reference, config) + manifest = ( + self.repository.generation_directory(reference.generation_id) + / MANIFEST_FILE + ) + manifest.write_text("{}", encoding="utf-8") + + with self.assertRaisesRegex(IndexSchemaError, "integrity"): + self.repository.read_active(required=True) + + def test_repository_lease_is_scoped_to_one_repository(self): + other = LocalSnapshotRepository( + Path(self.temporary.name) / "other" / "indexes" + ) + with self.repository.lease(): + self.assertTrue(self.repository.mutation_in_progress()) + self.assertFalse(other.mutation_in_progress()) + + def test_snapshot_configuration_moves_between_runtime_devices(self): + config, reference = self.generation("a", input_sha="a" * 64) + snapshot = self.repository.publish_generation(reference, config) + + active, loaded = self.repository.active_config(device="cuda") + + self.assertEqual(loaded.snapshot_id, snapshot.snapshot_id) + self.assertEqual(active.device, "cuda") + self.assertEqual(len(active.snapshot_sha256), 64) + self.assertEqual( + active.fingerprint(), + snapshot.config_fingerprint, + ) + + def test_historical_snapshot_configuration_reopens_by_checksum(self): + config_a, reference_a = self.generation("a", input_sha="a" * 64) + snapshot_a = self.repository.publish_generation(reference_a, config_a) + active_a, _ = self.repository.active_config(device="cpu") + config_b, reference_b = self.generation("b", input_sha="b" * 64) + snapshot_b = self.repository.publish_generation(reference_b, config_b) + + historical = self.repository.config_for_snapshot( + snapshot_a.snapshot_id, + snapshot_sha256=active_a.snapshot_sha256, + device="cuda", + ) + + self.assertNotEqual(snapshot_a.snapshot_id, snapshot_b.snapshot_id) + self.assertEqual(historical.snapshot_id, snapshot_a.snapshot_id) + self.assertEqual( + historical.snapshot_sha256, + active_a.snapshot_sha256, + ) + self.assertEqual(historical.device, "cuda") + self.assertEqual( + historical.fingerprint(), + snapshot_a.config_fingerprint, + ) + + def test_profile_mismatch_is_rejected_before_replacing_other_media(self): + config_a, a = self.generation("a", input_sha="a" * 64) + self.repository.publish_generation(a, config_a) + config_b, b = self.generation("b", input_sha="b" * 64) + self.repository.publish_generation(b, config_b) + + changed = replace( + config_a, + frame_stride=2, + generation_id=self.repository.new_generation_id(), + ) + changed = replace( + changed, + generation_directory=self.repository.generation_directory( + changed.generation_id + ), + ) + changed_ref = self.write_generation( + changed, + media_id="a", + input_sha="c" * 64, + ) + with self.assertRaisesRegex(IndexSchemaError, "same index profile"): + self.repository.publish_generation(changed_ref, changed) + + def test_snapshot_documents_are_canonical_json_objects(self): + config, reference = self.generation("a", input_sha="a" * 64) + snapshot = self.repository.publish_generation(reference, config) + payload = json.loads( + ( + self.repository.snapshots / f"{snapshot.snapshot_id}.json" + ).read_text(encoding="utf-8") + ) + self.assertEqual(payload["snapshot_id"], snapshot.snapshot_id) + + def test_build_failure_or_cancellation_preserves_active_snapshot(self): + config, reference = self.generation("a", input_sha="a" * 64) + active = self.repository.publish_generation(reference, config) + pointer_before = self.repository.active_pointer.read_bytes() + backend = LocalIndexBackend( + Mock(), + Mock(), + self.repository.layout, + ) + build = IndexConfig.local( + video_id="b" * 64, + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=self.repository.indexes, + ) + + for failure in ( + RuntimeError("injected build failure"), + IndexCancelledError("injected cancellation"), + ): + storage = MagicMock() + storage.__enter__.return_value = storage + storage.count_records.return_value = 0 + + def fail_index(*_args, **kwargs): + write_json_atomic( + kwargs["config"].run_directory / MANIFEST_FILE, + {"state": "interrupted"}, + ) + raise failure + + with ( + self.subTest(failure=type(failure).__name__), + patch( + "vidxp.infrastructure.local_index.IndexStorage", + return_value=storage, + ), + patch( + "vidxp.infrastructure.local_index.index_video", + side_effect=fail_index, + ), + self.assertRaises(type(failure)), + ): + backend.create( + Path("video.mp4"), + config=build, + progress=None, + cancellation=None, + source_name=None, + source_checksum="b" * 64, + ) + + self.assertEqual( + self.repository.active_pointer.read_bytes(), + pointer_before, + ) + self.assertEqual( + self.repository.read_active(required=True).snapshot_id, + active.snapshot_id, + ) + retained = { + path.name + for path in self.repository.generations.iterdir() + if path.is_dir() + } + self.assertEqual(retained, {reference.generation_id}) + + def test_reader_can_finish_against_snapshot_resolved_before_reindex(self): + config_a1, a1 = self.generation("a", input_sha="a" * 64) + first = self.repository.publish_generation(a1, config_a1) + old_config, _ = self.repository.active_config(device="cpu") + config_a2, a2 = self.generation("a", input_sha="b" * 64) + self.repository.publish_generation(a2, config_a2) + backend = LocalIndexBackend( + Mock(), + Mock(), + self.repository.layout, + ) + storage = MagicMock() + storage.records.return_value = [] + storage.count_records.return_value = 0 + + with patch( + "vidxp.infrastructure.local_index.IndexStorage", + return_value=storage, + ): + scoped_store = backend.open_store(old_config) + storage.reset_mock() + with scoped_store as scoped: + scoped.records("scene") + + self.assertNotEqual( + self.repository.read_active(required=True).snapshot_id, + first.snapshot_id, + ) + storage.records.assert_called_once_with( + "scene", + video_id=None, + generation_ids=(a1.generation_id,), + filters=None, + ) + + def test_reader_rejects_tampered_resolved_snapshot(self): + config, reference = self.generation("a", input_sha="a" * 64) + self.repository.publish_generation(reference, config) + resolved, _ = self.repository.active_config(device="cpu") + snapshot_path = ( + self.repository.snapshots / f"{resolved.snapshot_id}.json" + ) + snapshot_path.write_text("{}\n", encoding="utf-8") + backend = LocalIndexBackend( + Mock(), + Mock(), + self.repository.layout, + ) + + with self.assertRaisesRegex(IndexSchemaError, "integrity"): + backend.open_store(resolved) + + def test_reader_fails_closed_after_committed_record_loss(self): + generation_id = self.repository.new_generation_id() + config = replace( + self.config, + video_id="a", + generation_id=generation_id, + generation_directory=self.repository.generation_directory( + generation_id + ), + ) + with IndexStorage(config) as storage: + storage.upsert( + "scene", + [ + StorageRecord( + source_id="source-1", + embedding=[1.0, 0.0], + metadata={ + **config.record_identity("scene", "source-1"), + }, + ) + ], + batch_size=1, + cancellation=CancellationToken(), + ) + reference = self.write_generation( + config, + media_id="a", + input_sha="a" * 64, + record_counts={"scene": 1}, + ) + self.repository.publish_generation(reference, config) + active, _ = self.repository.active_config(device="cpu") + runtime = Mock() + runtime.backends.torch_device = "cpu" + backend = LocalIndexBackend( + Mock(), + runtime, + self.repository.layout, + ) + + with backend.open_store(active): + pass + with IndexStorage(config) as storage: + storage.delete_generation(generation_id) + + with backend.open_store(active): + pass + with self.assertRaisesRegex(IndexSchemaError, "record counts"): + backend.status(self.repository.indexes) + + def test_cancellation_after_validation_does_not_publish(self): + config_a, reference_a = self.generation("a", input_sha="a" * 64) + previous = self.repository.publish_generation(reference_a, config_a) + pointer_before = self.repository.active_pointer.read_bytes() + runtime = Mock() + runtime.backends.torch_device = "cpu" + backend = LocalIndexBackend( + Mock(), + runtime, + self.repository.layout, + ) + build = IndexConfig.local( + video_id="b" * 64, + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=self.repository.indexes, + ) + storage = MagicMock() + storage.__enter__.return_value = storage + storage.records.return_value = [] + storage.count_records.return_value = 0 + cancellation = Mock() + cancellation.raise_if_cancelled.side_effect = ( + None, + None, + IndexCancelledError("late cancellation"), + ) + + def complete_index(*_args, **kwargs): + self.write_generation( + kwargs["config"], + media_id="b" * 64, + input_sha="b" * 64, + ) + return {} + + with ( + patch( + "vidxp.infrastructure.local_index.IndexStorage", + return_value=storage, + ), + patch( + "vidxp.infrastructure.local_index.index_video", + side_effect=complete_index, + ), + self.assertRaises(IndexCancelledError), + ): + backend.create( + Path("video.mp4"), + config=build, + progress=None, + cancellation=cancellation, + source_name=None, + source_checksum="b" * 64, + ) + + self.assertEqual( + self.repository.active_pointer.read_bytes(), + pointer_before, + ) + self.assertEqual( + self.repository.read_active(required=True).snapshot_id, + previous.snapshot_id, + ) + + def test_abandoned_generation_directory_and_records_are_cleaned(self): + generation_id = self.repository.new_generation_id() + generation_directory = self.repository.generation_directory( + generation_id + ) + write_json_atomic( + generation_directory / MANIFEST_FILE, + {"state": "running", "generation_id": generation_id}, + ) + config = IndexConfig.local( + video_id="a", + generation_id=generation_id, + generation_directory=generation_directory, + enabled_modalities=("scene",), + collection_names={"scene": "scene"}, + storage_directory=self.repository.store, + ) + with IndexStorage(config) as storage: + storage.upsert( + "scene", + [ + StorageRecord( + source_id="abandoned-source", + embedding=[1.0], + metadata={ + **config.record_identity( + "scene", + "abandoned-source", + ), + }, + ) + ], + batch_size=1, + cancellation=CancellationToken(), + ) + + LocalIndexBackend._cleanup_abandoned_generations( + self.repository, + config, + ) + + self.assertFalse(generation_directory.exists()) + with IndexStorage(config, create=False) as storage: + self.assertEqual(storage.records("scene"), []) + + def test_real_chroma_reader_remains_pinned_across_reindex(self): + references = [] + for input_sha, embedding in ( + ("a" * 64, [1.0, 0.0]), + ("b" * 64, [0.0, 1.0]), + ): + generation_id = self.repository.new_generation_id() + config = replace( + self.config, + video_id="a", + generation_id=generation_id, + generation_directory=self.repository.generation_directory( + generation_id + ), + ) + with IndexStorage(config) as storage: + storage.upsert( + "scene", + [ + StorageRecord( + source_id=f"source-{generation_id}", + embedding=embedding, + metadata={ + **config.record_identity( + "scene", + f"source-{generation_id}", + ), + }, + ) + ], + batch_size=1, + cancellation=CancellationToken(), + ) + reference = self.write_generation( + config, + media_id="a", + input_sha=input_sha, + record_counts={"scene": 1}, + ) + snapshot = self.repository.publish_generation(reference, config) + active_config, _ = self.repository.active_config(device="cpu") + references.append( + (reference, snapshot, active_config) + ) + + runtime = Mock() + runtime.backends.torch_device = "cpu" + backend = LocalIndexBackend( + Mock(), + runtime, + self.repository.layout, + ) + first_reference, _, first_config = references[0] + second_reference, _, second_config = references[1] + with backend.open_store(first_config) as first_reader: + first_records = first_reader.records("scene") + with backend.open_store(second_config) as second_reader: + second_records = second_reader.records("scene") + + self.assertEqual( + {record["generation_id"] for record in first_records}, + {first_reference.generation_id}, + ) + self.assertEqual( + {record["generation_id"] for record in second_records}, + {second_reference.generation_id}, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_local_worker.py b/tests/test_local_worker.py new file mode 100644 index 0000000..9971436 --- /dev/null +++ b/tests/test_local_worker.py @@ -0,0 +1,485 @@ +import os +import re +import subprocess +import sys +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from threading import Event +from time import sleep +from unittest.mock import Mock, patch + +from filelock import Timeout +from vidxp import __version__ +from vidxp.application_models import ( + ApplicationError, + JobState, + PrepareModelsCommand, +) +from vidxp.infrastructure.dbos_jobs import DBOSJobBackend +from vidxp.infrastructure.local_worker import LocalWorkerSupervisor +from vidxp.job_service import JobService +from vidxp.settings import LocalExecutionSettings, VidXPSettings +from vidxp.workflow_worker import _configure_logging, run_worker +from vidxp.workflow_runtime import ( + LOCAL_WORKER_BOOTSTRAP_ENV, + LocalWorkerBootstrap, + LocalWorkerReady, + LocalWorkerStopRequest, + local_worker_bootstrap, + local_executor_id, + server_executor_id, + workflow_application_version, + workflow_database_url, +) +from vidxp.workflow_worker import main as workflow_worker_main + + +class LocalWorkerSupervisorTests(unittest.TestCase): + def test_workflow_version_identifies_the_release_and_implementation(self): + value = workflow_application_version() + + self.assertRegex( + value, + rf"^{re.escape(__version__)}\+[0-9a-f]{{16}}$", + ) + + def test_server_executor_identity_keeps_the_single_worker_ordinal(self): + version = workflow_application_version() + + self.assertEqual(server_executor_id(role="cpu"), f"{version}-cpu-0") + self.assertEqual(server_executor_id(role="gpu"), f"{version}-gpu-0") + with self.assertRaisesRegex(ValueError, "role"): + server_executor_id(role="other") + + def test_worker_is_spawned_detached_without_job_state(self): + with TemporaryDirectory() as directory: + settings = VidXPSettings( + repository_root=Path(directory), + runtime_backend="cpu", + http_auth_mode="static", + http_static_bearer_token="s" * 32, + ) + supervisor = LocalWorkerSupervisor(settings) + captured_bootstrap = None + + def spawn(command, **options): + nonlocal captured_bootstrap + bootstrap_path = Path( + options["env"][LOCAL_WORKER_BOOTSTRAP_ENV] + ) + captured_bootstrap = LocalWorkerBootstrap.model_validate_json( + bootstrap_path.read_text(encoding="utf-8") + ) + process = Mock() + process.poll.return_value = None + return process + + with ( + patch( + "vidxp.infrastructure.local_worker.subprocess.Popen", + side_effect=spawn, + ) as popen, + patch.object(supervisor, "_wait_for_worker") as wait, + patch.dict( + os.environ, + { + "VIDXP_HTTP_AUTH_MODE": "static", + "VIDXP_HTTP_STATIC_BEARER_TOKEN": "e" * 32, + "VIDXP_DATABASE_URL": ( + "postgresql://user:password@db/vidxp" + ), + "DBOS__CLOUD": "true", + "DBOS_SYSTEM_DATABASE_URL": ( + "postgresql://other:secret@db/other" + ), + }, + ), + ): + supervisor.ensure_running() + + command = popen.call_args.args[0] + options = popen.call_args.kwargs + self.assertEqual( + wait.call_args.kwargs["timeout_seconds"], + supervisor._startup_timeout_seconds, + ) + self.assertGreaterEqual(supervisor._startup_timeout_seconds, 10) + self.assertEqual(command[:3], [sys.executable, "-m", "vidxp.workflow_worker"]) + self.assertIn("--lock-file", command) + lock_file = command[command.index("--lock-file") + 1] + self.assertIn(workflow_application_version(), lock_file) + self.assertNotIn("--database-url", command) + self.assertNotIn("password", " ".join(command)) + self.assertIs(options["stdin"], subprocess.DEVNULL) + self.assertIs(options["stdout"], subprocess.DEVNULL) + self.assertIs(options["stderr"], subprocess.DEVNULL) + self.assertFalse( + any( + key.upper().startswith(("VIDXP_", "DBOS_")) + for key in options["env"] + if key != LOCAL_WORKER_BOOTSTRAP_ENV + ) + ) + self.assertIsNotNone(captured_bootstrap) + serialized = captured_bootstrap.settings.model_dump_json() + self.assertNotIn("s" * 32, serialized) + self.assertNotIn("e" * 32, serialized) + self.assertNotIn("http_static_bearer_token", serialized) + worker_settings = captured_bootstrap.settings.application_settings() + self.assertEqual( + worker_settings.repository_root, + settings.repository_root, + ) + self.assertEqual(worker_settings.http_auth_mode, "none") + wait.assert_called_once() + if sys.platform == "win32": + self.assertEqual( + options["creationflags"], + ( + subprocess.CREATE_NEW_PROCESS_GROUP + | subprocess.CREATE_NO_WINDOW + ), + ) + else: + self.assertTrue(options["start_new_session"]) + + def test_application_settings_ignore_inherited_vidxp_environment(self): + local = LocalExecutionSettings.from_settings( + VidXPSettings(runtime_backend="cpu") + ) + with patch.dict( + os.environ, + { + "VIDXP_HTTP_AUTH_MODE": "static", + "VIDXP_HTTP_STATIC_BEARER_TOKEN": "e" * 32, + "VIDXP_DATABASE_URL": ( + "postgresql://user:password@db/vidxp" + ), + }, + ): + settings = local.application_settings() + + self.assertEqual(settings.http_auth_mode, "none") + self.assertIsNone(settings.http_static_bearer_token) + self.assertNotIn("database_url", type(settings).model_fields) + + def test_startup_wait_finishes_when_worker_owns_lock(self): + with TemporaryDirectory() as directory: + ready_path = Path(directory) / "worker.ready" + ready_path.write_text("123", encoding="utf-8") + process = Mock() + process.poll.return_value = None + worker_lock = Mock() + worker_lock.acquire.side_effect = Timeout("worker.lock") + fingerprint = "a" * 64 + ready_path.write_text( + LocalWorkerReady( + pid=123, + application_version=workflow_application_version(), + fingerprint=fingerprint, + ).model_dump_json(), + encoding="utf-8", + ) + + LocalWorkerSupervisor._wait_for_worker( + process, + worker_lock, + ready_path, + fingerprint=fingerprint, + ) + + worker_lock.acquire.assert_called_once_with(timeout=0) + + def test_failed_startup_is_backed_off(self): + with TemporaryDirectory() as directory: + supervisor = LocalWorkerSupervisor( + VidXPSettings(repository_root=Path(directory)) + ) + with patch.object( + supervisor, + "_ensure_running", + side_effect=RuntimeError("failed"), + ) as start: + with self.assertRaisesRegex(RuntimeError, "failed"): + supervisor.ensure_running() + with self.assertRaisesRegex(RuntimeError, "backoff"): + supervisor.ensure_running() + + start.assert_called_once_with() + + def test_stale_worker_configuration_is_rejected(self): + with TemporaryDirectory() as directory: + ready_path = Path(directory) / "worker.ready" + ready_path.write_text( + LocalWorkerReady( + pid=123, + application_version=workflow_application_version(), + fingerprint="a" * 64, + ).model_dump_json(), + encoding="utf-8", + ) + worker_lock = Mock() + worker_lock.acquire.side_effect = Timeout("worker.lock") + + with self.assertRaisesRegex( + RuntimeError, + "different execution settings", + ): + LocalWorkerSupervisor._wait_for_existing_worker( + worker_lock, + ready_path, + fingerprint="b" * 64, + timeout_seconds=0.1, + ) + + def test_startup_timeout_terminates_exact_spawned_process(self): + with TemporaryDirectory() as directory: + supervisor = LocalWorkerSupervisor( + VidXPSettings(repository_root=Path(directory)) + ) + process = Mock() + process.poll.return_value = None + with ( + patch( + "vidxp.infrastructure.local_worker.subprocess.Popen", + return_value=process, + ), + patch.object( + supervisor, + "_wait_for_worker", + side_effect=RuntimeError("startup timed out"), + ), + self.assertRaisesRegex(RuntimeError, "startup timed out"), + ): + supervisor.ensure_running() + + process.terminate.assert_called_once_with() + process.wait.assert_called_once_with(timeout=5) + + def test_stale_versioned_bootstrap_is_removed_before_start(self): + with TemporaryDirectory() as directory: + supervisor = LocalWorkerSupervisor( + VidXPSettings(repository_root=Path(directory)) + ) + supervisor.layout.ensure_local_directories() + stale = ( + supervisor.layout.local_workflows + / ( + f".worker-{workflow_application_version()}-" + "bootstrap-stale.json" + ) + ) + stale.write_text("database-secret", encoding="utf-8") + + supervisor._remove_stale_bootstraps( + workflow_application_version() + ) + + self.assertFalse(stale.exists()) + + def test_worker_recomputes_bootstrap_identity(self): + with TemporaryDirectory() as directory: + settings = VidXPSettings( + repository_root=Path(directory), + runtime_backend="cpu", + ) + supervisor = LocalWorkerSupervisor(settings) + supervisor.layout.ensure_local_directories() + bootstrap = local_worker_bootstrap(settings) + tampered = bootstrap.model_copy( + update={ + "settings": bootstrap.settings.model_copy( + update={"runtime_backend": "cuda"} + ) + } + ) + path = supervisor._write_bootstrap(tampered) + with ( + patch.dict( + os.environ, + {LOCAL_WORKER_BOOTSTRAP_ENV: str(path)}, + ), + patch("vidxp.workflow_worker.run_worker") as run, + self.assertRaisesRegex(ValueError, "identity is invalid"), + ): + workflow_worker_main( + ["--executor-id", "test", "--role", "all"] + ) + + run.assert_not_called() + self.assertFalse(path.exists()) + + def test_stop_terminates_only_ready_lock_owner(self): + with TemporaryDirectory() as directory: + settings = VidXPSettings(repository_root=Path(directory)) + supervisor = LocalWorkerSupervisor(settings) + supervisor.layout.ensure_local_directories() + ready_path = ( + supervisor.layout.local_workflows + / f"worker-{workflow_application_version()}.ready" + ) + ready_path.write_text( + LocalWorkerReady( + pid=123, + application_version=workflow_application_version(), + fingerprint="a" * 64, + ).model_dump_json(), + encoding="utf-8", + ) + worker_lock = Mock() + worker_lock.acquire.side_effect = [ + Timeout("worker.lock"), + None, + ] + with ( + patch( + "vidxp.infrastructure.local_worker.FileLock", + return_value=worker_lock, + ), + patch( + "vidxp.infrastructure.local_worker.write_json_atomic" + ) as write_stop, + ): + stopped = supervisor.stop() + + self.assertTrue(stopped) + write_stop.assert_called_once() + self.assertEqual(write_stop.call_args.args[1]["pid"], 123) + self.assertEqual( + write_stop.call_args.args[1]["application_version"], + workflow_application_version(), + ) + self.assertFalse(ready_path.exists()) + + def test_worker_destroys_dbos_after_stop_request(self): + stop_event = Event() + stop_event.set() + + with ( + patch("vidxp.workflow_worker.DBOS") as dbos, + patch("vidxp.workflow_worker.VidXPWorkerWorkflows"), + patch("vidxp.workflow_worker.create_application"), + ): + run_worker( + settings=VidXPSettings(runtime_backend="cpu"), + database_url="sqlite:///test.db", + executor_id="test", + role="cpu", + stop_event=stop_event, + ) + + dbos.destroy.assert_called_once_with( + workflow_completion_timeout_sec=30 + ) + + def test_worker_uses_live_rotating_log_handler(self): + with TemporaryDirectory() as directory: + log_path = Path(directory) / "worker.log" + with ( + patch( + "vidxp.workflow_worker.RotatingFileHandler" + ) as handler, + patch("vidxp.workflow_worker.logging.basicConfig") as config, + ): + _configure_logging(log_path) + + handler.assert_called_once_with( + log_path, + maxBytes=5 * 1024 * 1024, + backupCount=2, + encoding="utf-8", + ) + config.assert_called_once() + + def test_fresh_database_runs_a_job_in_a_separate_worker_process(self): + with TemporaryDirectory(ignore_cleanup_errors=True) as directory: + settings = VidXPSettings( + repository_root=Path(directory), + runtime_backend="cpu", + allow_model_downloads=False, + workflow_poll_interval_seconds=0.01, + http_auth_mode="static", + http_static_bearer_token="s" * 32, + ) + settings.layout.ensure_local_directories() + database_url = workflow_database_url(settings) + ready_path = settings.layout.local_workflows / "test.ready" + lock_path = settings.layout.local_workflows / "test.lock" + stop_path = settings.layout.local_workflows / "test.stop" + supervisor = LocalWorkerSupervisor(settings) + bootstrap = local_worker_bootstrap(settings) + bootstrap_path = supervisor._write_bootstrap(bootstrap) + environment = { + key: value + for key, value in os.environ.items() + if not key.upper().startswith("VIDXP_") + } + environment[LOCAL_WORKER_BOOTSTRAP_ENV] = str(bootstrap_path) + process = subprocess.Popen( + [ + sys.executable, + "-m", + "vidxp.workflow_worker", + "--executor-id", + local_executor_id(settings), + "--role", + "all", + "--lock-file", + str(lock_path), + "--ready-file", + str(ready_path), + "--stop-file", + str(stop_path), + "--log-file", + str(settings.layout.local_workflows / "test.log"), + ], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=environment, + ) + backend = None + try: + for _ in range(200): + if ready_path.is_file(): + break + if process.poll() is not None: + self.fail("The separate worker exited during startup.") + sleep(0.05) + else: + self.fail("The separate worker did not become ready.") + + backend = DBOSJobBackend( + system_database_url=database_url, + application_version=workflow_application_version(), + ) + jobs = JobService(settings=settings, backend=backend) + submitted = jobs.submit_prepare_models( + PrepareModelsCommand(modalities=()) + ) + with self.assertRaises(ApplicationError): + jobs.wait(submitted.job_id) + completed = jobs.get(submitted.job_id) + + self.assertEqual(completed.state, JobState.failed) + self.assertEqual(completed.error.code, "invalid_request") + finally: + if backend is not None: + backend.close() + stop_path.write_text( + LocalWorkerStopRequest( + pid=process.pid, + application_version=workflow_application_version(), + ).model_dump_json(), + encoding="utf-8", + ) + try: + process.wait(timeout=35) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 6db7d5f..6313443 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -2,17 +2,50 @@ import unittest from pathlib import Path from tempfile import TemporaryDirectory -from unittest.mock import patch +from unittest.mock import Mock, patch from vidxp.core.contracts import IndexConfig, VideoSource from vidxp.core.manifest import ( ManifestStore, source_checksums, + sync_parent_directory, write_json_atomic, ) +from vidxp.capabilities.registry import create_capability_registry +from vidxp.runtime import ModelRuntime +from vidxp.settings import VidXPSettings class ManifestIdentityTests(unittest.TestCase): + def test_unsupported_directory_fsync_does_not_false_fail_commit(self): + with ( + patch("vidxp.core.manifest.os.name", "posix"), + patch("vidxp.core.manifest.os.open", return_value=17), + patch( + "vidxp.core.manifest.os.fsync", + side_effect=OSError("unsupported"), + ), + patch("vidxp.core.manifest.os.close") as close, + ): + sync_parent_directory(Path("repository")) + + close.assert_called_once_with(17) + + def test_atomic_write_leaves_no_temporary_file(self): + with TemporaryDirectory() as directory: + path = Path(directory) / "manifest.json" + + write_json_atomic(path, {"state": "ready"}) + + self.assertEqual( + path.read_text(encoding="utf-8"), + '{\n "state": "ready"\n}\n', + ) + self.assertEqual( + list(path.parent.glob(f".{path.name}.*.tmp")), + [], + ) + def test_atomic_write_retries_transient_windows_reader_lock(self): with TemporaryDirectory() as directory: path = Path(directory) / "manifest.json" @@ -33,6 +66,32 @@ def test_atomic_write_retries_transient_windows_reader_lock(self): self.assertEqual(replace.call_count, 3) self.assertEqual(sleep.call_count, 2) + def test_atomic_write_failure_preserves_target_and_cleans_temporary_file( + self, + ): + with TemporaryDirectory() as directory: + path = Path(directory) / "manifest.json" + original = '{\n "state": "ready"\n}\n' + path.write_text(original, encoding="utf-8") + + with ( + patch.object( + Path, + "replace", + side_effect=OSError("replace failed"), + ) as replace, + patch("vidxp.core.manifest.time.sleep"), + self.assertRaisesRegex(OSError, "replace failed"), + ): + write_json_atomic(path, {"state": "running"}) + + self.assertEqual(replace.call_count, 5) + self.assertEqual(path.read_text(encoding="utf-8"), original) + self.assertEqual( + list(path.parent.glob(f".{path.name}.*.tmp")), + [], + ) + def test_declared_video_checksum_does_not_suppress_transcript_hash(self): transcript = ({"text": "hello", "start": 0.0, "end": 1.0},) checksums = source_checksums( @@ -55,7 +114,16 @@ def test_checkpoint_filenames_do_not_embed_dataset_video_ids(self): output_root=directory, enabled_modalities=("scene",), ) - store = ManifestStore(config) + store = ManifestStore( + config, + registry=create_capability_registry(), + runtime=ModelRuntime( + VidXPSettings( + repository_root=config.run_directory, + runtime_backend="cpu", + ) + ), + ) video_id = "folder/name:video" expected = hashlib.sha256(video_id.encode("utf-8")).hexdigest() @@ -68,6 +136,75 @@ def test_checkpoint_filenames_do_not_embed_dataset_video_ids(self): / f"{expected}.json", ) + def test_terminal_manifests_refresh_resolved_runtime_identity(self): + for terminal in ("fail_video", "interrupt_video"): + with self.subTest(terminal=terminal): + with TemporaryDirectory() as directory: + config = IndexConfig( + dataset="sample", + split="test", + run_id="run-1", + output_root=directory, + enabled_modalities=("scene",), + ) + runtime = Mock() + runtime.describe.return_value = { + "resolved_models": {}, + } + store = ManifestStore( + config, + registry=create_capability_registry(), + runtime=runtime, + ) + source = VideoSource( + video_id="video-1", + transcript=( + {"text": "hello", "start": 0.0, "end": 1.0}, + ), + ) + with patch( + "vidxp.core.manifest.execution_state", + return_value={ + "git": {"commit": None, "dirty": None}, + "implementation_sha256": "test", + "package_version": "test", + "python": "test", + "platform": "test", + "dependencies": {}, + }, + ): + store.initialize( + [ + ( + "video-1", + source, + "a" * 64, + {"declared": "a" * 64}, + ) + ] + ) + store.start_video("video-1") + runtime.describe.return_value = { + "resolved_models": {"scene": {"cached": True}}, + "compute_precision": {"scene": "float32"}, + } + + if terminal == "fail_video": + store.fail_video("video-1", "scene", "failed") + else: + store.interrupt_video("video-1", "scene") + + self.assertEqual( + store.read()["models"]["runtime"]["resolved_models"], + {"scene": {"cached": True}}, + ) + self.assertEqual( + store.read()["models"]["runtime"][ + "compute_precision" + ], + {"scene": "float32"}, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_media_catalog.py b/tests/test_media_catalog.py new file mode 100644 index 0000000..9718a1d --- /dev/null +++ b/tests/test_media_catalog.py @@ -0,0 +1,451 @@ +import json +import os +import subprocess +import unittest +from datetime import datetime, timezone +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from vidxp.core.artifacts import ArtifactKind, ArtifactRecord +from vidxp.core.media import ( + MediaImportLimitError, + MediaRecord, + MediaState, + MediaStream, +) +from vidxp.infrastructure.local_catalog import LocalCatalog +from vidxp.infrastructure.local_artifacts import LocalArtifactStore +from vidxp.infrastructure.local_media import ( + FFprobeMediaProbe, + InvalidMediaError, + LocalMediaStore, +) +from vidxp.infrastructure.local_files import prepare_managed_destination +from vidxp.infrastructure.local_objects import LocalObjectStore + + +MEDIA_ID = "123456781234423481234567890abcde" +OTHER_MEDIA_ID = "223456781234423481234567890abcde" +ARTIFACT_ID = "323456781234423481234567890abcde" + + +def media_record( + media_id: str = MEDIA_ID, + *, + checksum: str = "1" * 64, +) -> MediaRecord: + return MediaRecord( + media_id=media_id, + video_id=media_id, + sha256=checksum, + original_filename="video.mp4", + byte_size=5, + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + storage_key=f"objects/{checksum[:2]}/{checksum}.mp4", + state=MediaState.ready, + created_at=datetime.now(timezone.utc), + ) + + +class LocalCatalogTests(unittest.TestCase): + def test_catalog_enforces_sqlite_integrity_and_schema_version(self): + with TemporaryDirectory() as directory: + database = Path(directory) / "catalog.sqlite3" + catalog = LocalCatalog(database) + with catalog.engine.connect() as connection: + self.assertEqual( + set( + connection.exec_driver_sql( + "SELECT name FROM sqlite_master " + "WHERE type = 'table'" + ).scalars() + ), + { + "artifact_requests", + "artifacts", + "catalog_metadata", + "media", + "media_import_requests", + }, + ) + self.assertEqual( + connection.exec_driver_sql( + "PRAGMA foreign_keys" + ).scalar_one(), + 1, + ) + self.assertEqual( + connection.exec_driver_sql( + "PRAGMA busy_timeout" + ).scalar_one(), + 30_000, + ) + with catalog.transaction() as connection: + connection.exec_driver_sql( + "UPDATE catalog_metadata SET schema_version = 999" + ) + catalog.close() + + with self.assertRaisesRegex(RuntimeError, "incompatible"): + LocalCatalog(database) + + def test_catalog_persists_and_deduplicates_media_by_checksum(self): + with TemporaryDirectory() as directory: + database = Path(directory) / "catalog.sqlite3" + first = LocalCatalog(database) + record = media_record() + self.assertEqual(first.put_media(record), record) + + reopened = LocalCatalog(database) + self.assertEqual(reopened.get_media(MEDIA_ID), record) + duplicate = media_record(OTHER_MEDIA_ID) + self.assertEqual(reopened.put_media(duplicate), record) + self.assertEqual(reopened.list_media(limit=10), (record,)) + + def test_artifact_requires_cataloged_media_and_survives_reopen(self): + with TemporaryDirectory() as directory: + database = Path(directory) / "catalog.sqlite3" + catalog = LocalCatalog(database) + catalog.put_media(media_record()) + artifact = ArtifactRecord( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + request_key="3" * 64, + kind=ArtifactKind.actor_overlay, + profile="default", + mime_type="video/mp4", + byte_size=10, + sha256="2" * 64, + storage_key=f"objects/32/{ARTIFACT_ID}.mp4", + created_at=datetime.now(timezone.utc), + ) + + catalog.put_artifact(artifact) + + self.assertEqual( + LocalCatalog(database).get_artifact(ARTIFACT_ID), + artifact, + ) + self.assertEqual( + LocalCatalog(database).get_artifact_by_request("3" * 64), + artifact, + ) + + +class LocalMediaStoreTests(unittest.TestCase): + def test_import_is_staged_hashed_once_and_atomically_published(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"video") + store = LocalMediaStore(root / "media", max_bytes=100) + + staged = store.stage_local(source) + self.assertEqual(staged.byte_size, 5) + self.assertEqual( + staged.sha256, + "0cab1c9617404faf2b24e221e189ca5945813e14" + "d3f766345b09ca13bbe28ffc", + ) + self.assertTrue(staged.path.is_file()) + + with store.publication_lock(staged.sha256): + stored = store.publish(staged) + + self.assertFalse(staged.path.exists()) + self.assertEqual(store.resolve(stored.storage_key), stored.local_path) + self.assertEqual(stored.local_path.read_bytes(), b"video") + + def test_size_limit_and_unsafe_storage_keys_fail_closed(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"too-large") + store = LocalMediaStore(root / "media", max_bytes=3) + + with self.assertRaisesRegex(MediaImportLimitError, "import limit"): + store.stage_local(source) + with self.assertRaises(ValueError): + store.resolve("../outside.mp4") + + def test_managed_storage_rejects_symlink_escapes(self): + with TemporaryDirectory() as directory: + root = Path(directory) + media_root = root / "media" + (media_root / "objects").mkdir(parents=True) + outside = root / "outside" + outside.mkdir() + (outside / "secret.mp4").write_bytes(b"secret") + link = media_root / "objects" / "escape" + try: + link.symlink_to(outside, target_is_directory=True) + except OSError as exc: + self.skipTest(f"Symlink creation is unavailable: {exc}") + + store = LocalMediaStore(media_root, max_bytes=100) + with self.assertRaises((FileNotFoundError, PermissionError)): + store.resolve("objects/escape/secret.mp4") + + def test_destination_rejects_junction_parent(self): + with TemporaryDirectory() as directory: + root = Path(directory) + junction = root / "objects" / "junction" + junction.mkdir(parents=True) + original = getattr(Path, "is_junction", lambda _item: False) + + with patch.object( + Path, + "is_junction", + lambda item: item == junction or original(item), + create=True, + ): + with self.assertRaises(PermissionError): + prepare_managed_destination( + root, + "objects/junction/video.mp4", + ) + + def test_destination_syncs_each_new_directory_entry(self): + with TemporaryDirectory() as directory: + root = Path(directory) / "media" + with patch( + "vidxp.infrastructure.local_files.sync_parent_directory" + ) as sync: + prepare_managed_destination( + root, + "objects/aa/video.mp4", + ) + + self.assertEqual( + [call.args[0] for call in sync.call_args_list], + [ + root.parent, + root, + root / "objects", + ], + ) + + def test_failed_publication_rollback_syncs_parent(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "staged.mp4" + source.write_bytes(b"video") + store = LocalObjectStore(root / "objects") + destination = store.root / "aa" / "video.mp4" + destination.parent.mkdir(parents=True) + original_resolve = Path.resolve + + def resolve(path, *args, **kwargs): + if path == destination: + raise OSError("post-publication validation failed") + return original_resolve(path, *args, **kwargs) + + with ( + patch.object(Path, "resolve", resolve), + patch( + "vidxp.infrastructure.local_files." + "sync_parent_directory" + ) as sync, + self.assertRaisesRegex( + OSError, + "post-publication validation failed", + ), + ): + store.publish( + source, + "aa/video.mp4", + expected_sha256=None, + replace_corrupt=False, + ) + + self.assertFalse(destination.exists()) + self.assertEqual( + [call.args[0] for call in sync.call_args_list], + [destination.parent, root, destination.parent], + ) + + def test_publication_rejects_symlinked_parent(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"video") + store = LocalMediaStore(root / "media", max_bytes=100) + staged = store.stage_local(source) + first_parent = store.root / "objects" / staged.sha256[:2] + outside = root / "outside" + outside.mkdir() + first_parent.parent.mkdir(parents=True) + try: + first_parent.symlink_to(outside, target_is_directory=True) + except OSError as exc: + store.discard(staged) + self.skipTest(f"Symlink creation is unavailable: {exc}") + + with self.assertRaises(PermissionError): + store.publish(staged) + self.assertEqual(tuple(outside.iterdir()), ()) + store.discard(staged) + + def test_reimport_repairs_same_size_corruption(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"video") + store = LocalMediaStore(root / "media", max_bytes=100) + first = store.publish(store.stage_local(source)) + first.local_path.write_bytes(b"bideo") + + repaired = store.publish(store.stage_local(source)) + + self.assertEqual(repaired.local_path.read_bytes(), b"video") + self.assertEqual( + store.verify( + repaired.storage_key, + sha256=repaired.sha256, + byte_size=repaired.byte_size, + ), + repaired.local_path, + ) + + @unittest.skipUnless(os.name == "nt", "Windows junction behavior") + def test_publication_rejects_windows_junction_parent(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"video") + store = LocalMediaStore(root / "media", max_bytes=100) + staged = store.stage_local(source) + parent = store.objects / staged.sha256[:2] + parent.parent.mkdir(parents=True) + outside = root / "outside" + outside.mkdir() + result = subprocess.run( + [ + "cmd", + "/c", + "mklink", + "/J", + str(parent), + str(outside), + ], + check=False, + capture_output=True, + text=True, + ) + if result.returncode: + store.discard(staged) + self.skipTest("Junction creation is unavailable.") + + with self.assertRaises(PermissionError): + store.publish(staged) + self.assertEqual(tuple(outside.iterdir()), ()) + store.discard(staged) + + +class FFprobeTests(unittest.TestCase): + def test_probe_normalizes_stream_and_container_metadata(self): + payload = { + "format": {"format_name": "mov,mp4", "duration": "2.5"}, + "streams": [ + { + "index": 0, + "codec_type": "video", + "codec_name": "h264", + "width": 1920, + "height": 1080, + }, + { + "index": 1, + "codec_type": "audio", + "codec_name": "aac", + "channels": 2, + "sample_rate": "48000", + }, + ], + } + with patch.object( + FFprobeMediaProbe, + "_output", + return_value=json.dumps(payload).encode(), + ): + result = FFprobeMediaProbe().probe(Path("video.mp4")) + + self.assertEqual(result.container, "mp4") + self.assertEqual(result.detected_mime_type, "video/mp4") + self.assertEqual(result.streams[0].codec, "h264") + self.assertEqual(result.streams[1].sample_rate, 48000) + + def test_probe_rejects_malformed_or_non_video_results(self): + with patch.object( + FFprobeMediaProbe, + "_output", + return_value=b'{"format":{"format_name":"mp4","duration":"nan"}}', + ): + with self.assertRaises(InvalidMediaError): + FFprobeMediaProbe().probe(Path("bad.mp4")) + + def test_probe_distinguishes_matroska_from_webm_by_extension(self): + payload = { + "format": { + "format_name": "matroska,webm", + "duration": "2.5", + }, + "streams": [ + { + "index": 0, + "codec_type": "video", + "codec_name": "h264", + "width": 1920, + "height": 1080, + } + ], + } + with patch.object( + FFprobeMediaProbe, + "_output", + return_value=json.dumps(payload).encode(), + ): + matroska = FFprobeMediaProbe().probe(Path("video.mkv")) + webm = FFprobeMediaProbe().probe(Path("video.webm")) + + self.assertEqual(matroska.detected_mime_type, "video/x-matroska") + self.assertEqual(webm.detected_mime_type, "video/webm") + + +class LocalArtifactStoreTests(unittest.TestCase): + def test_publication_rejects_symlinked_parent(self): + with TemporaryDirectory() as directory: + root = Path(directory) + store = LocalArtifactStore(root / "artifacts") + staged = store.stage(ARTIFACT_ID, suffix=".mp4") + staged.path.write_bytes(b"video") + outside = root / "outside" + outside.mkdir() + parent = store.objects / ARTIFACT_ID[:2] + parent.parent.mkdir(parents=True) + try: + parent.symlink_to(outside, target_is_directory=True) + except OSError as exc: + store.discard(staged) + self.skipTest(f"Symlink creation is unavailable: {exc}") + + with self.assertRaises(PermissionError): + store.publish(staged) + self.assertEqual(tuple(outside.iterdir()), ()) + store.discard(staged) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_media_runtime.py b/tests/test_media_runtime.py new file mode 100644 index 0000000..8c7523a --- /dev/null +++ b/tests/test_media_runtime.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import os +import sys +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from vidxp.media_runtime import ( + MediaRuntimeStatus, + inspect_media_runtime, + load_media_runtime_configuration, + media_runtime_is_initialized, + save_media_runtime_configuration, + system_install_plan, +) + + +class MediaRuntimeTests(unittest.TestCase): + def test_inspection_verifies_both_executables_and_required_encoders(self): + ffmpeg = Path("resolved/ffmpeg").resolve() + ffprobe = Path("resolved/ffprobe").resolve() + + def resolve(value): + return ffmpeg if str(value) == "ffmpeg" else ffprobe + + def output(arguments, **_kwargs): + if "-encoders" in arguments: + return " V....D libx264\n A....D aac\n" + return "version" + + with TemporaryDirectory() as temporary_directory: + with ( + patch("vidxp.media_runtime._resolve_executable", side_effect=resolve), + patch("vidxp.media_runtime._command_output", side_effect=output), + patch("vidxp.media_runtime.system_install_plan"), + ): + status = inspect_media_runtime( + config_directory=Path(temporary_directory) + ) + + self.assertTrue(status.ready) + self.assertEqual(status.ffmpeg_executable, ffmpeg) + self.assertEqual(status.ffprobe_executable, ffprobe) + self.assertEqual(status.errors, ()) + + def test_inspection_rejects_an_ffmpeg_build_without_required_codecs(self): + executable = Path("resolved/tool").resolve() + + def output(arguments, **_kwargs): + return " V....D libx264\n" if "-encoders" in arguments else "version" + + with ( + patch( + "vidxp.media_runtime._resolve_executable", + return_value=executable, + ), + patch("vidxp.media_runtime._command_output", side_effect=output), + patch("vidxp.media_runtime.system_install_plan", return_value=None), + ): + status = inspect_media_runtime() + + self.assertFalse(status.ready) + self.assertIn("aac", " ".join(status.errors)) + + def test_verified_absolute_paths_are_persisted_and_reloaded(self): + with TemporaryDirectory() as directory: + root = Path(directory) + ffmpeg = root / "ffmpeg.exe" + ffprobe = root / "ffprobe.exe" + ffmpeg.touch() + ffprobe.touch() + status = MediaRuntimeStatus( + ready=True, + initialized=False, + ffmpeg_executable=ffmpeg.resolve(), + ffprobe_executable=ffprobe.resolve(), + ) + + saved = save_media_runtime_configuration( + status, + config_directory=root / "config", + ) + loaded = load_media_runtime_configuration(root / "config") + + self.assertEqual(loaded, saved) + self.assertTrue(saved.ffmpeg_executable.is_absolute()) + self.assertTrue(saved.ffprobe_executable.is_absolute()) + + def test_initialized_accepts_saved_config_environment_or_path(self): + with TemporaryDirectory() as directory: + root = Path(directory) + ffmpeg = root / "ffmpeg" + ffprobe = root / "ffprobe" + ffmpeg.touch() + ffprobe.touch() + save_media_runtime_configuration( + MediaRuntimeStatus( + ready=True, + initialized=False, + ffmpeg_executable=ffmpeg.resolve(), + ffprobe_executable=ffprobe.resolve(), + ), + config_directory=root, + ) + self.assertTrue(media_runtime_is_initialized(root)) + + with patch.dict( + os.environ, + { + "VIDXP_FFMPEG_EXECUTABLE": "custom-ffmpeg", + "VIDXP_FFPROBE_EXECUTABLE": "custom-ffprobe", + }, + clear=False, + ): + self.assertTrue(media_runtime_is_initialized(Path("missing"))) + + with ( + patch.dict(os.environ, {}, clear=True), + patch( + "vidxp.media_runtime.load_media_runtime_configuration", + return_value=None, + ), + patch("vidxp.media_runtime.shutil.which", return_value=None), + ): + self.assertFalse(media_runtime_is_initialized()) + + @unittest.skipUnless(sys.platform == "win32", "Windows package contract") + def test_windows_plan_uses_the_approved_exact_winget_package(self): + with patch("vidxp.media_runtime.shutil.which", return_value="winget"): + plan = system_install_plan() + + self.assertIsNotNone(plan) + assert plan is not None + self.assertEqual(plan.manager, "Windows Package Manager") + self.assertTrue(plan.automatic) + self.assertIn("Gyan.FFmpeg", plan.command) + self.assertIn("--exact", plan.command) + self.assertIn("--source", plan.command) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_media_services.py b/tests/test_media_services.py new file mode 100644 index 0000000..5121994 --- /dev/null +++ b/tests/test_media_services.py @@ -0,0 +1,827 @@ +import unittest +from contextlib import nullcontext +from datetime import datetime, timezone +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import Mock, patch + +from vidxp.application_models import ( + ActorOverlayProfile, + CreateSnippetCommand, + ImportMediaCommand, + ListMediaCommand, + SnippetProfile, +) +from vidxp.artifact_service import ( + ArtifactService, + ArtifactUnavailableError, + InvalidArtifactError, +) +from vidxp.core.artifacts import ArtifactState +from vidxp.core.contracts import CancellationToken, IndexCancelledError +from vidxp.core.media import ( + MediaProbe, + QuarantinedMedia, + MediaRecord, + MediaState, + MediaStream, + StagedMedia, + StoredMedia, +) +from vidxp.execution import ExecutionContext +from vidxp.infrastructure.local_artifacts import ( + FFmpegSnippetRenderer, + LocalArtifactStore, +) +from vidxp.infrastructure.local_catalog import LocalCatalog +from vidxp.infrastructure.local_media import InvalidMediaError +from vidxp.media_service import ( + MediaIdempotencyConflictError, + MediaService, +) +from vidxp.ports import LocalFileResource +from vidxp.settings import VidXPSettings + + +MEDIA_ID = "123456781234423481234567890abcde" +JOB_ID = "223456781234423481234567890abcde" +ARTIFACT_ID = "323456781234423481234567890abcde" + + +def record() -> MediaRecord: + return MediaRecord( + media_id=MEDIA_ID, + video_id=MEDIA_ID, + sha256="1" * 64, + original_filename="video.mp4", + byte_size=5, + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + storage_key="objects/11/video.mp4", + state=MediaState.ready, + created_at=datetime.now(timezone.utc), + ) + + +class MediaServiceTests(unittest.TestCase): + def service(self, root: Path): + catalog = Mock() + store = Mock() + store.publication_lock.return_value = nullcontext() + probe = Mock() + service = MediaService( + settings=VidXPSettings( + repository_root=root, + runtime_backend="cpu", + ), + catalog=catalog, + store=store, + probe=probe, + ) + return service, catalog, store, probe + + def test_import_probes_staging_before_publishing_and_cataloging(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "video.mp4" + source.write_bytes(b"video") + service, catalog, store, probe = self.service(root) + staged = StagedMedia( + sha256="1" * 64, + byte_size=5, + storage_key="objects/11/video.mp4", + path=root / "staged.tmp", + ) + stored = StoredMedia( + sha256=staged.sha256, + byte_size=5, + storage_key=staged.storage_key, + local_path=root / "managed.mp4", + ) + store.stage_local.return_value = staged + store.publish.return_value = stored + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + catalog.get_media_by_checksum.return_value = None + catalog.put_media.side_effect = lambda item: item + with patch("vidxp.media_service.uuid4") as identifier: + identifier.return_value.hex = MEDIA_ID + result = service.import_local(ImportMediaCommand(path=source)) + + self.assertEqual(result.media_id, MEDIA_ID) + store.publication_lock.assert_called_once_with(staged.sha256) + probe.probe.assert_called_once_with(staged.path) + store.publish.assert_called_once_with(staged) + catalog.put_media.assert_called_once() + store.discard.assert_called_once_with(staged) + + def test_invalid_probe_never_publishes_or_catalogs_media(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "video.mp4" + source.write_bytes(b"video") + service, catalog, store, probe = self.service(root) + staged = StagedMedia( + sha256="1" * 64, + byte_size=5, + storage_key="objects/11/video.mp4", + path=root / "staged.tmp", + ) + store.stage_local.return_value = staged + catalog.get_media_by_checksum.return_value = None + probe.probe.side_effect = InvalidMediaError("invalid") + + with self.assertRaises(InvalidMediaError): + service.import_local(ImportMediaCommand(path=source)) + + store.publish.assert_not_called() + catalog.put_media.assert_not_called() + store.discard.assert_called_once_with(staged) + + def test_quarantined_import_reuses_the_same_ingestion_pipeline(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "upload.mp4" + source.write_bytes(b"video") + service, catalog, store, probe = self.service(root) + staged = StagedMedia( + sha256="1" * 64, + byte_size=5, + storage_key="objects/11/video.mp4", + path=root / "staged.tmp", + ) + stored = StoredMedia( + sha256=staged.sha256, + byte_size=5, + storage_key=staged.storage_key, + local_path=root / "managed.mp4", + ) + store.stage_local.return_value = staged + store.publish.return_value = stored + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + catalog.get_media_by_checksum.return_value = None + catalog.put_media.side_effect = lambda item: item + with patch("vidxp.media_service.uuid4") as identifier: + identifier.return_value.hex = MEDIA_ID + result = service.import_quarantined( + QuarantinedMedia( + path=source, + original_filename="client-name.mp4", + declared_mime_type="video/mp4", + ) + ) + + self.assertEqual(result.original_filename, "client-name.mp4") + store.stage_local.assert_called_once_with(source.resolve()) + probe.probe.assert_called_once_with(staged.path) + store.publish.assert_called_once_with(staged) + store.discard.assert_called_once_with(staged) + + def test_quarantined_import_completes_durable_idempotency_record(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "upload.mp4" + source.write_bytes(b"video") + service, catalog, store, probe = self.service(root) + staged = StagedMedia( + sha256="1" * 64, + byte_size=5, + storage_key="objects/11/video.mp4", + path=root / "staged.tmp", + ) + stored = StoredMedia( + sha256=staged.sha256, + byte_size=5, + storage_key=staged.storage_key, + local_path=root / "managed.mp4", + ) + store.stage_local.return_value = staged + store.publish.return_value = stored + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + catalog.reserve_media_import.return_value = None + catalog.get_media_by_checksum.return_value = None + imported = record() + catalog.put_media.return_value = imported + catalog.get_media.return_value = imported + + result = service.import_quarantined( + QuarantinedMedia( + path=source, + original_filename="upload.mp4", + ), + request_key="request-key", + ) + + self.assertEqual(result.media_id, MEDIA_ID) + fingerprint = catalog.reserve_media_import.call_args.args[1] + self.assertEqual(len(fingerprint), 64) + catalog.complete_media_import.assert_called_once_with( + "request-key", + fingerprint, + imported, + ) + + def test_quarantined_import_rejects_reused_key_for_other_content(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "upload.mp4" + source.write_bytes(b"video") + service, catalog, store, _probe = self.service(root) + staged = StagedMedia( + sha256="2" * 64, + byte_size=5, + storage_key="objects/22/video.mp4", + path=root / "staged.tmp", + ) + store.stage_local.return_value = staged + catalog.reserve_media_import.side_effect = FileExistsError + + with self.assertRaises(MediaIdempotencyConflictError): + service.import_quarantined( + QuarantinedMedia( + path=source, + original_filename="upload.mp4", + ), + request_key="request-key", + ) + + store.publish.assert_not_called() + + def test_local_catalog_persists_media_import_idempotency(self): + with TemporaryDirectory() as directory: + catalog = LocalCatalog(Path(directory) / "catalog.sqlite3") + item = record() + catalog.put_media(item) + + self.assertIsNone( + catalog.reserve_media_import( + "request-key", + "f" * 64, + ) + ) + catalog.complete_media_import( + "request-key", + "f" * 64, + item, + ) + replay = catalog.reserve_media_import( + "request-key", + "f" * 64, + ) + + self.assertEqual(replay, item) + with self.assertRaises(FileExistsError): + catalog.reserve_media_import( + "request-key", + "e" * 64, + ) + + def test_existing_checksum_is_reused_without_reprobe(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "video.mp4" + source.write_bytes(b"video") + service, catalog, store, probe = self.service(root) + staged = StagedMedia( + sha256="1" * 64, + byte_size=5, + storage_key="objects/11/video.mp4", + path=root / "staged.tmp", + ) + store.stage_local.return_value = staged + catalog.get_media_by_checksum.return_value = record() + + result = service.import_local(ImportMediaCommand(path=source)) + + self.assertEqual(result.media_id, MEDIA_ID) + probe.probe.assert_not_called() + store.publish.assert_called_once_with(staged) + store.discard.assert_called_once_with(staged) + + def test_catalog_failure_rolls_back_new_managed_content(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "video.mp4" + source.write_bytes(b"video") + service, catalog, store, probe = self.service(root) + staged = StagedMedia( + sha256="1" * 64, + byte_size=5, + storage_key="objects/11/video.mp4", + path=root / "staged.tmp", + ) + stored = StoredMedia( + sha256=staged.sha256, + byte_size=5, + storage_key=staged.storage_key, + local_path=root / "managed.mp4", + ) + store.stage_local.return_value = staged + store.publish.return_value = stored + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + catalog.get_media_by_checksum.return_value = None + catalog.put_media.side_effect = RuntimeError("catalog failed") + + with self.assertRaisesRegex(RuntimeError, "catalog failed"): + service.import_local(ImportMediaCommand(path=source)) + + store.delete.assert_called_once_with(stored.storage_key) + + def test_media_pages_are_bounded_and_cursor_scoped(self): + with TemporaryDirectory() as directory: + service, catalog, _store, _probe = self.service(Path(directory)) + catalog.count_media.return_value = 3 + catalog.list_media.side_effect = [ + (record(), record().model_copy( + update={ + "media_id": "223456781234423481234567890abcde", + "video_id": "223456781234423481234567890abcde", + "sha256": "2" * 64, + } + )), + (record().model_copy( + update={ + "media_id": "323456781234423481234567890abcde", + "video_id": "323456781234423481234567890abcde", + "sha256": "3" * 64, + } + ),), + ] + + first = service.list(ListMediaCommand(page_size=2)) + second = service.list( + ListMediaCommand(page_size=2, cursor=first.next_cursor) + ) + + self.assertEqual(first.total, 3) + self.assertEqual(len(first.items), 2) + self.assertIsNotNone(first.next_cursor) + self.assertEqual(len(second.items), 1) + self.assertIsNone(second.next_cursor) + self.assertEqual( + catalog.list_media.call_args_list[1].kwargs["offset"], + 2, + ) + + +class ArtifactServiceTests(unittest.TestCase): + def test_actor_overlay_uses_catalog_media_and_store_owned_destination(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"source") + media = Mock() + media.content.return_value = LocalFileResource( + path=source, + filename="source.mp4", + mime_type="video/mp4", + byte_size=6, + etag="1" * 64, + ) + catalog = Mock() + catalog.get_artifact_by_request.return_value = None + catalog.put_artifact.side_effect = lambda item: item + renderer = Mock() + renderer.render.side_effect = ( + lambda _source, destination, _cluster, _detections, **_kwargs: + destination.write_bytes(b"rendered") + ) + probe = Mock() + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=1, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + store = LocalArtifactStore(root / "artifacts") + service = ArtifactService( + catalog=catalog, + store=store, + media=media, + probe=probe, + actor_renderer=renderer, + snippet_renderer=Mock(), + max_snippet_duration_seconds=300, + ) + with patch("vidxp.artifact_service.uuid4") as identifier: + identifier.return_value.hex = ARTIFACT_ID + result = service.create_actor_overlay( + media_id=MEDIA_ID, + generation_id="223456781234423481234567890abcde", + cluster_id="cluster", + detections=[{"frame_index": 1}], + profile=ActorOverlayProfile.default, + ) + + self.assertEqual(result.artifact_id, ARTIFACT_ID) + self.assertEqual(result.media_id, MEDIA_ID) + self.assertEqual(result.byte_size, len(b"rendered")) + renderer.render.assert_called_once() + self.assertNotIn("path", result.model_dump(mode="json")) + catalog.put_artifact.assert_called_once() + + def test_progress_failure_after_catalog_commit_keeps_artifact_content(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"source") + media = Mock() + media.content.return_value = LocalFileResource( + path=source, + filename="source.mp4", + mime_type="video/mp4", + byte_size=6, + etag="1" * 64, + ) + catalog = Mock() + catalog.get_artifact_by_request.return_value = None + catalog.put_artifact.side_effect = lambda item: item + renderer = Mock() + renderer.render.side_effect = ( + lambda _source, destination, _cluster, _detections, **_kwargs: + destination.write_bytes(b"rendered") + ) + probe = Mock() + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=1, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + store = LocalArtifactStore(root / "artifacts") + service = ArtifactService( + catalog=catalog, + store=store, + media=media, + probe=probe, + actor_renderer=renderer, + snippet_renderer=Mock(), + max_snippet_duration_seconds=300, + ) + + def fail_final_progress(event): + if event["stage"] == "complete": + raise RuntimeError("progress unavailable") + + with ( + patch("vidxp.artifact_service.uuid4") as identifier, + self.assertRaisesRegex(RuntimeError, "progress unavailable"), + ): + identifier.return_value.hex = ARTIFACT_ID + service.create_actor_overlay( + media_id=MEDIA_ID, + generation_id="223456781234423481234567890abcde", + cluster_id="cluster", + detections=[], + profile=ActorOverlayProfile.default, + execution=ExecutionContext(progress=fail_final_progress), + ) + + committed = catalog.put_artifact.call_args.args[0] + self.assertTrue(store.resolve(committed.storage_key).is_file()) + + def test_durable_replay_recovers_object_published_before_catalog_commit(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"source") + media = Mock() + media.content.return_value = LocalFileResource( + path=source, + filename="source.mp4", + mime_type="video/mp4", + byte_size=6, + etag="1" * 64, + ) + catalog = Mock() + catalog.get_artifact_by_request.return_value = None + catalog.put_artifact.side_effect = lambda item: item + renderer = Mock() + probe = Mock() + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=1, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + store = LocalArtifactStore(root / "artifacts") + orphan = store.stage(JOB_ID, suffix=".mp4") + orphan.path.write_bytes(b"rendered-before-crash") + store.publish(orphan) + service = ArtifactService( + catalog=catalog, + store=store, + media=media, + probe=probe, + actor_renderer=renderer, + snippet_renderer=Mock(), + max_snippet_duration_seconds=300, + ) + + result = service.create_actor_overlay( + media_id=MEDIA_ID, + generation_id="423456781234423481234567890abcde", + cluster_id="cluster", + detections=[], + profile=ActorOverlayProfile.default, + job_id=JOB_ID, + execution=ExecutionContext(job_id=JOB_ID), + ) + + self.assertEqual(result.artifact_id, JOB_ID) + renderer.render.assert_not_called() + catalog.put_artifact.assert_called_once() + + def test_ffmpeg_snippet_renderer_terminates_on_cancellation(self): + cancellation = CancellationToken() + cancellation.cancel() + process = Mock() + process.poll.return_value = None + process.wait.return_value = 0 + renderer = FFmpegSnippetRenderer() + + with ( + patch( + "vidxp.infrastructure.local_artifacts.subprocess.Popen", + return_value=process, + ), + self.assertRaises(IndexCancelledError), + ): + renderer.render( + Path("source.mp4"), + Path("snippet.mp4"), + start_seconds=0, + end_seconds=1, + compatible_mp4=True, + cancellation=cancellation, + progress=None, + ) + + process.terminate.assert_called_once() + + def test_actor_renderer_failure_leaves_no_staged_or_ready_artifact(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"source") + media = Mock() + media.content.return_value = LocalFileResource( + path=source, + filename="source.mp4", + mime_type="video/mp4", + byte_size=6, + etag="1" * 64, + ) + catalog = Mock() + catalog.get_artifact_by_request.return_value = None + renderer = Mock() + renderer.render.side_effect = RuntimeError("render failed") + store = LocalArtifactStore(root / "artifacts") + service = ArtifactService( + catalog=catalog, + store=store, + media=media, + probe=Mock(), + actor_renderer=renderer, + snippet_renderer=Mock(), + max_snippet_duration_seconds=300, + ) + with ( + patch("vidxp.artifact_service.uuid4") as identifier, + self.assertRaisesRegex(RuntimeError, "render failed"), + ): + identifier.return_value.hex = ARTIFACT_ID + service.create_actor_overlay( + media_id=MEDIA_ID, + generation_id="223456781234423481234567890abcde", + cluster_id="cluster", + detections=[], + profile=ActorOverlayProfile.default, + ) + + self.assertFalse(any(store.staging.glob("*"))) + catalog.put_artifact.assert_not_called() + + def test_invalid_rendered_media_is_not_published_or_cataloged(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"source") + media = Mock() + media.content.return_value = LocalFileResource( + path=source, + filename="source.mp4", + mime_type="video/mp4", + byte_size=6, + etag="1" * 64, + ) + catalog = Mock() + catalog.get_artifact_by_request.return_value = None + renderer = Mock() + renderer.render.side_effect = ( + lambda _source, destination, _cluster, _detections, **_kwargs: + destination.write_bytes(b"not-video") + ) + probe = Mock() + probe.probe.side_effect = InvalidMediaError("invalid") + store = LocalArtifactStore(root / "artifacts") + service = ArtifactService( + catalog=catalog, + store=store, + media=media, + probe=probe, + actor_renderer=renderer, + snippet_renderer=Mock(), + max_snippet_duration_seconds=300, + ) + + with self.assertRaises(InvalidArtifactError): + service.create_actor_overlay( + media_id=MEDIA_ID, + generation_id="223456781234423481234567890abcde", + cluster_id="cluster", + detections=[], + profile=ActorOverlayProfile.default, + ) + + self.assertFalse(any(store.staging.glob("*"))) + catalog.put_artifact.assert_not_called() + + def test_snippet_is_bounded_and_reuses_ready_request(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.mp4" + source.write_bytes(b"source") + media = Mock() + media.require_record.return_value = record() + media.content.return_value = LocalFileResource( + path=source, + filename="source.mp4", + mime_type="video/mp4", + byte_size=6, + etag="1" * 64, + ) + catalog = Mock() + catalog.get_artifact_by_request.return_value = None + catalog.put_artifact.side_effect = lambda item: item + renderer = Mock() + renderer.render.side_effect = ( + lambda _source, destination, **_kwargs: + destination.write_bytes(b"rendered") + ) + probe = Mock() + probe.probe.return_value = MediaProbe( + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=1, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + ) + store = LocalArtifactStore(root / "artifacts") + service = ArtifactService( + catalog=catalog, + store=store, + media=media, + probe=probe, + actor_renderer=Mock(), + snippet_renderer=renderer, + max_snippet_duration_seconds=1, + ) + command = CreateSnippetCommand( + media_id=MEDIA_ID, + start_seconds=0, + end_seconds=1, + profile=SnippetProfile.compatible_mp4, + ) + + created = service.create_snippet(command) + catalog.get_artifact_by_request.return_value = ( + catalog.put_artifact.call_args.args[0] + ) + reused = service.create_snippet(command) + cached_record = catalog.put_artifact.call_args.args[0] + store.delete(cached_record.storage_key) + regenerated = service.create_snippet(command) + + self.assertEqual(reused.artifact_id, created.artifact_id) + self.assertNotEqual(regenerated.artifact_id, created.artifact_id) + self.assertEqual(renderer.render.call_count, 2) + catalog.invalidate_artifact_request.assert_called_once_with( + cached_record.request_key, + cached_record.artifact_id, + ) + + def test_expired_artifact_is_unavailable(self): + catalog = Mock() + expired = Mock() + catalog.get_artifact.return_value = expired + expired.state = ArtifactState.ready + expired.expires_at = datetime(2000, 1, 1, tzinfo=timezone.utc) + service = ArtifactService( + catalog=catalog, + store=Mock(), + media=Mock(), + probe=Mock(), + actor_renderer=Mock(), + snippet_renderer=Mock(), + max_snippet_duration_seconds=1, + ) + + with self.assertRaises(ArtifactUnavailableError): + service.require_record(ARTIFACT_ID) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_models.py b/tests/test_models.py index 50c144a..74af819 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,110 +1,496 @@ +import hashlib +import os import sys import types import unittest +from concurrent.futures import ThreadPoolExecutor +from dataclasses import replace from importlib.metadata import PackageNotFoundError -from unittest.mock import Mock, patch +from pathlib import Path +from tempfile import TemporaryDirectory +from threading import Barrier +from time import sleep +from unittest.mock import Mock, call, patch + +from packaging.requirements import Requirement +from pydantic import ValidationError from vidxp.capabilities.contracts import CapabilityDefinition from vidxp.capabilities.dialogue import models as dialogue_models -from vidxp.capabilities.registry import ( - dependency_checks, - requirements_for, - runtime_checks_for, - runtime_distributions, +from vidxp.capabilities.dialogue.specs import ( + FASTER_WHISPER_MODEL, + QWEN3_EMBEDDING_MODEL, ) -from vidxp.core.contracts import VideoSource -from vidxp.dependencies import inspect_requirement -from packaging.requirements import Requirement +from vidxp.capabilities.actor import models as actor_models +from vidxp.capabilities.actor.definition import model_manifest as actor_manifest +from vidxp.capabilities.actor.specs import SFACE_MODEL, YUNET_MODEL +from vidxp.capabilities.registry import create_capability_registry +from vidxp.application_models import DependencyKind +from vidxp.core.contracts import IndexConfig, VideoSource +from vidxp.dependencies import ( + active_requirements, + inspect_requirement, + packaged_requirements, +) +from vidxp.infrastructure.local_index import ( + LOCAL_INDEX_RUNTIME_CHECKS, + SERVER_INDEX_RUNTIME_CHECKS, +) +from vidxp.model_contracts import ( + ModelArtifactUnavailableError, + ModelKey, + model_artifact_path, +) +from vidxp.runtime import ModelRuntime, resolve_backends +from vidxp.settings import VidXPSettings class ModelTests(unittest.TestCase): - def tearDown(self): - dialogue_models.clear_model_cache() + def runtime( + self, + root: str, + *, + allowed_specs=None, + allow_model_downloads: bool | None = None, + ) -> ModelRuntime: + settings = { + "repository_root": root, + "model_cache": Path(root) / "models", + "runtime_backend": "cpu", + } + if allow_model_downloads is not None: + settings["allow_model_downloads"] = allow_model_downloads + return ModelRuntime( + VidXPSettings(**settings), + allowed_specs=( + create_capability_registry().model_specs() + if allowed_specs is None + else tuple(allowed_specs) + ), + ) - def test_dialogue_model_is_reused_across_videos(self): - constructor = Mock(return_value=object()) - fake_module = types.SimpleNamespace(SentenceTransformer=constructor) - with patch.dict(sys.modules, {"sentence_transformers": fake_module}): - first = dialogue_models.get_embedder("model-id", "cpu") - second = dialogue_models.get_embedder("model-id", "cpu") + def test_model_runtime_reuses_one_provider_instance(self): + with TemporaryDirectory() as directory: + runtime = self.runtime(directory) + constructor = Mock(return_value=object()) + fake_module = types.SimpleNamespace( + SentenceTransformer=constructor + ) + snapshot = Path(directory) / "snapshot" + runtime.resolve_model = Mock(return_value=snapshot) + with patch.dict( + sys.modules, + {"sentence_transformers": fake_module}, + ): + first = dialogue_models.get_embedder(runtime) + second = dialogue_models.get_embedder(runtime) self.assertIs(first, second) - constructor.assert_called_once_with("model-id", device="cpu") + constructor.assert_called_once_with( + str(snapshot), + device="cpu", + cache_folder=str(Path(directory) / "models"), + local_files_only=True, + ) + self.assertEqual( + runtime.describe()["compute_precision"]["dialogue.embedding"], + "bfloat16", + ) + + def test_unrelated_model_loads_do_not_hold_the_global_cache_lock(self): + runtime = self.runtime("unused") + barrier = Barrier(2) + + def load(value): + barrier.wait(timeout=2) + return value + + keys = ( + ModelKey("scene", "one", "one", "1", "cpu"), + ModelKey("dialogue", "two", "two", "1", "cpu"), + ) + with ThreadPoolExecutor(max_workers=2) as pool: + futures = [ + pool.submit(runtime.get_or_load, key, lambda v=value: load(v)) + for value, key in enumerate(keys) + ] + self.assertEqual([future.result() for future in futures], [0, 1]) + + def test_actor_download_uses_pinned_media_object_not_lfs_pointer(self): + with TemporaryDirectory() as directory: + content = b"verified model" + downloaded = Path(directory) / "model.onnx" + downloaded.write_bytes(content) + spec = replace( + YUNET_MODEL, + filename=downloaded.name, + sha256=hashlib.sha256(content).hexdigest(), + ) + runtime = self.runtime( + directory, + allowed_specs=(spec,), + allow_model_downloads=True, + ) + retrieve = Mock(return_value=str(downloaded)) + fake_pooch = types.SimpleNamespace(retrieve=retrieve) + with patch.dict(sys.modules, {"pooch": fake_pooch}): + result = runtime.resolve_artifact(spec, download=True) + + self.assertEqual(result, downloaded) + url = retrieve.call_args.kwargs["url"] + self.assertIn("media.githubusercontent.com/media/", url) + self.assertIn(spec.revision, url) + self.assertNotIn("raw.githubusercontent.com", url) + self.assertEqual( + retrieve.call_args.kwargs["known_hash"], + f"sha256:{spec.sha256}", + ) + self.assertEqual( + runtime.describe()["resolved_models"]["actor.detector"]["model"], + "yunet", + ) + + def test_normal_model_resolution_never_downloads_implicitly(self): + with TemporaryDirectory() as directory: + spec = replace(YUNET_MODEL, filename="missing.onnx") + runtime = self.runtime( + directory, + allowed_specs=(spec,), + allow_model_downloads=True, + ) + retrieve = Mock(side_effect=AssertionError("downloaded")) + with ( + patch.dict( + sys.modules, + {"pooch": types.SimpleNamespace(retrieve=retrieve)}, + ), + self.assertRaises(ModelArtifactUnavailableError), + ): + runtime.resolve_artifact(spec) + + retrieve.assert_not_called() + + def test_model_readiness_checks_the_pinned_cache_without_loading(self): + with TemporaryDirectory() as directory: + cache = Path(directory) / "models" + path = model_artifact_path(cache, YUNET_MODEL) + path.parent.mkdir(parents=True) + path.write_bytes(b"present") + checks = create_capability_registry().model_checks( + ("actor",), + cache=cache, + ) + + self.assertEqual( + [ + (check.name, check.download_size_bytes, check.ok) + for check in checks + ], + [ + ( + YUNET_MODEL.model_id, + YUNET_MODEL.download_size_bytes, + True, + ), + ( + SFACE_MODEL.model_id, + SFACE_MODEL.download_size_bytes, + False, + ), + ], + ) + + def test_explicit_snapshot_download_reports_bytes(self): + with TemporaryDirectory() as directory: + snapshot = Path(directory) / "snapshot" + events = [] + + def download(**options): + progress = options["tqdm_class"]( + desc="Downloading bytes", + total=1024, + unit="B", + ) + progress.update(512) + sleep(0.6) + progress.update(512) + sleep(0.6) + progress.close() + return str(snapshot) + + with patch( + "huggingface_hub.snapshot_download", + side_effect=download, + ), patch( + "huggingface_hub.constants.HF_HUB_DISABLE_XET", + False, + ): + resolved = ModelRuntime._download_snapshot( + FASTER_WHISPER_MODEL, + cache=Path(directory), + progress=events.append, + ) + self.assertTrue( + __import__("huggingface_hub").constants.HF_HUB_DISABLE_XET + ) + + self.assertEqual(resolved, snapshot) + self.assertTrue( + any( + event["stage"] == "downloading_model" + and event["current"] == 1024 + and event["total"] == 1024 + for event in events + ) + ) + + def test_runtime_rejects_specs_not_declared_by_enabled_capabilities(self): + with TemporaryDirectory() as directory: + runtime = self.runtime(directory) + untrusted = replace(YUNET_MODEL, model_id="untrusted-detector") + + with self.assertRaises(ModelArtifactUnavailableError): + runtime.resolve_artifact(untrusted) + + self.assertEqual(runtime.describe()["resolved_models"], {}) + + def test_actor_artifact_resolves_verified_offline_cache(self): + with TemporaryDirectory() as directory: + content = b"verified cached model" + spec = replace( + YUNET_MODEL, + filename="cached.onnx", + sha256=hashlib.sha256(content).hexdigest(), + ) + cache = Path(directory) / "models" + path = cache / spec.provider / spec.filename + path.parent.mkdir(parents=True) + path.write_bytes(content) + runtime = ModelRuntime( + VidXPSettings( + repository_root=directory, + model_cache=cache, + runtime_backend="cpu", + allow_model_downloads=False, + ), + allowed_specs=(spec,), + ) + fake_pooch = types.SimpleNamespace( + retrieve=Mock(side_effect=AssertionError("downloaded")) + ) + with patch.dict(sys.modules, {"pooch": fake_pooch}): + resolved = runtime.resolve_artifact(spec) + + self.assertEqual(resolved, path) + self.assertTrue( + runtime.describe()["resolved_models"]["actor.detector"]["cached"] + ) + + def test_bad_or_missing_offline_artifact_is_not_recorded(self): + for content in (None, b"corrupt"): + with self.subTest(content=content): + with TemporaryDirectory() as directory: + cache = Path(directory) / "models" + spec = replace( + YUNET_MODEL, + filename="cached.onnx", + sha256=hashlib.sha256(b"expected").hexdigest(), + ) + if content is not None: + path = cache / spec.provider / spec.filename + path.parent.mkdir(parents=True) + path.write_bytes(content) + runtime = ModelRuntime( + VidXPSettings( + repository_root=directory, + model_cache=cache, + runtime_backend="cpu", + allow_model_downloads=False, + ), + allowed_specs=(spec,), + ) + with self.assertRaises(ModelArtifactUnavailableError): + runtime.resolve_artifact(spec) + + self.assertNotIn( + "actor.detector", + runtime.describe()["resolved_models"], + ) + + def test_actor_models_resolve_both_specs_through_runtime(self): + runtime = Mock() + runtime.device_for.return_value = "cpu" + runtime.get_or_load.side_effect = lambda _key, loader: loader() + runtime.resolve_artifact.side_effect = ( + Path("detector.onnx"), + Path("recognizer.onnx"), + ) + detector = Mock() + recognizer = Mock() + fake_cv2 = types.SimpleNamespace( + FaceDetectorYN=types.SimpleNamespace(create=detector), + FaceRecognizerSF=types.SimpleNamespace(create=recognizer), + ) - def test_scene_dependency_check_does_not_touch_other_capabilities(self): + with patch.dict(sys.modules, {"cv2": fake_cv2}): + actor_models.get_actor_models(runtime) + + self.assertEqual( + [call.args[0] for call in runtime.resolve_artifact.call_args_list], + [YUNET_MODEL, SFACE_MODEL], + ) + detector.assert_called_once() + recognizer.assert_called_once() + self.assertEqual( + runtime.record_compute_precision.call_args_list, + [ + call("actor.detector", "float32"), + call("actor.recognizer", "float32"), + ], + ) + + def test_actor_manifest_derives_provider_identity_from_specs(self): + manifest = actor_manifest(IndexConfig.local(), ()) + actor = manifest["actor"] + + self.assertEqual( + actor["models"], + { + "detector": YUNET_MODEL.identity(), + "recognizer": SFACE_MODEL.identity(), + }, + ) + for repeated in ("provider", "revision", "license", "precision"): + self.assertNotIn(repeated, actor) + + def test_model_spec_metadata_is_canonical_and_unambiguous(self): + self.assertEqual(YUNET_MODEL.license, "MIT") + self.assertEqual(SFACE_MODEL.license, "Apache-2.0") + self.assertEqual(QWEN3_EMBEDDING_MODEL.weights_precision, "bfloat16") + self.assertEqual( + FASTER_WHISPER_MODEL.model_id, + "dropbox-dash/faster-whisper-large-v3-turbo", + ) + self.assertEqual( + FASTER_WHISPER_MODEL.weights_precision, + "float16", + ) + + def test_scene_dependency_check_is_capability_scoped(self): + registry = create_capability_registry() inspected = [] - versions = { - "chromadb": "1.0", - "clip-anytorch": "2.6.0", - "numpy": "2.1", - "opencv-python": "4.10", - "Pillow": "10.0", - "setuptools": "80.10.2", - "torch": "2.5", - } + def installed(name): + inspected.append(name) + return { + "chromadb": "1.5.9", + "psutil": "7.2.2", + "numpy": "2.5.1", + "opencv-python-headless": "5.0.0.93", + "Pillow": "12.3.0", + "huggingface-hub": "1.25.1", + "torch": "2.13.0", + "transformers": "5.14.1", + }[name] - with patch( - "vidxp.dependencies.version", - side_effect=lambda name: ( - inspected.append(name), - versions[name], - )[1], - ): - checks = dependency_checks(("scene",)) + with patch("vidxp.dependencies.version", side_effect=installed): + checks = registry.dependency_checks(("scene",)) - self.assertTrue(all(check["ok"] for check in checks)) - self.assertNotIn("whisperx", inspected) - self.assertNotIn("face-recognition", inspected) + self.assertTrue( + all( + check.ok + for check in checks + if check.kind == DependencyKind.distribution + ) + ) + self.assertNotIn("faster-whisper", inspected) self.assertNotIn("sentence-transformers", inspected) + self.assertNotIn("pooch", inspected) + + def test_platform_runtime_checks_are_storage_owned_and_not_duplicated(self): + registry = create_capability_registry( + platform_runtime_checks=LOCAL_INDEX_RUNTIME_CHECKS + ) + + checks = registry.dependency_checks(registry.names()) + storage_checks = [ + check + for check in checks + if check.kind == DependencyKind.runtime + and check.capability == "storage" + ] + + self.assertEqual( + [check.name for check in storage_checks], + [ + "Chroma storage import", + "Host resource monitor import", + ], + ) - def test_supplied_transcript_only_requires_dialogue_search_dependencies(self): + def test_server_storage_checks_match_the_remote_chroma_client(self): + registry = create_capability_registry( + platform_runtime_checks=SERVER_INDEX_RUNTIME_CHECKS, + storage_requirements=active_requirements( + packaged_requirements( + "vidxp", + "requirements/server-storage.txt", + ) + ), + ) + + requirements = { + requirement.name + for requirement in registry.requirements_for(("scene",)) + } + + self.assertIn("chromadb-client", requirements) + self.assertNotIn("chromadb", requirements) + + def test_transcript_only_excludes_transcription_provider(self): + registry = create_capability_registry() source = VideoSource( transcript=({"text": "hello", "start": 0, "end": 1},) ) distributions = { requirement.name - for requirement in requirements_for( + for requirement in registry.requirements_for( ("dialogue",), source=source, ) } self.assertIn("sentence-transformers", distributions) - self.assertNotIn("whisperx", distributions) - self.assertNotIn("moviepy", distributions) - self.assertNotIn("opencv-python", distributions) + self.assertNotIn("faster-whisper", distributions) - def test_runtime_distributions_come_from_capability_registry(self): + def test_runtime_distributions_come_from_registry(self): + registry = create_capability_registry() with patch( "vidxp.capabilities.registry.installed_base_requirements", return_value=(), ): - distributions = runtime_distributions() + distributions = registry.runtime_distributions() - self.assertIn("clip-anytorch", distributions) - self.assertIn("face-recognition", distributions) + self.assertIn("transformers", distributions) + self.assertIn("faster-whisper", distributions) + self.assertIn("opencv-python-headless", distributions) + self.assertNotIn("clip-anytorch", distributions) + self.assertNotIn("face-recognition", distributions) self.assertEqual(len(distributions), len(set(distributions))) - def test_requirement_files_are_the_only_python_dependency_contract(self): + def test_requirement_files_are_dependency_contract(self): self.assertNotIn("dependencies", CapabilityDefinition.model_fields) - checks = runtime_checks_for( - ("dialogue",), - source=VideoSource( - transcript=({"text": "hello", "start": 0, "end": 1},) - ), - ) - self.assertEqual(checks, ()) - self.assertEqual( - [check.label for check in runtime_checks_for( - ("dialogue",), - source=VideoSource(path="video.mp4"), - )], - ["FFmpeg"], + registry = create_capability_registry() + self.assertIn( + "faster-whisper import", + { + check.label + for check in registry.runtime_checks_for(("dialogue",)) + }, ) - def test_requirement_check_uses_distribution_metadata_and_specifier(self): + def test_requirement_check_uses_distribution_metadata(self): requirement = Requirement("example>=2,<3") with patch("vidxp.dependencies.version", return_value="2.5"): self.assertTrue(inspect_requirement(requirement)["ok"]) @@ -116,6 +502,74 @@ def test_requirement_check_uses_distribution_metadata_and_specifier(self): ): self.assertFalse(inspect_requirement(requirement)["ok"]) + def test_server_runtime_and_external_allowlist_are_explicit(self): + with self.assertRaises(ValidationError): + VidXPSettings(mode="server", runtime_backend="mps") + with self.assertRaises(ValidationError): + VidXPSettings(capability_allowlist=("distribution-only",)) + settings = VidXPSettings( + mode="server", + runtime_backend="cuda:0", + capability_allowlist=("acme-capabilities:ocr",), + ) + self.assertEqual(settings.runtime_backend, "cuda:0") + self.assertNotIn("database_url", VidXPSettings.model_fields) + self.assertNotIn("chroma_server_url", VidXPSettings.model_fields) + with self.assertRaises(ValidationError): + VidXPSettings(slm_base_url="http://localhost:11434/v1") + with self.assertRaises(ValidationError): + VidXPSettings( + slm_base_url="https://ollama.com/v1", + slm_model="qwen3", + ) + with self.assertRaises(ValidationError): + VidXPSettings( + slm_base_url="http://localhost:11434/v1", + slm_model="qwen3-cloud", + ) + slm = VidXPSettings( + slm_base_url="http://localhost:11434/v1", + slm_model="evaluated-model", + ) + self.assertEqual(slm.slm_model, "evaluated-model") + + def test_only_optional_slm_environment_values_ignore_empty_strings(self): + with patch.dict( + os.environ, + { + "VIDXP_SLM_BASE_URL": "", + "VIDXP_SLM_MODEL": "", + }, + clear=True, + ): + settings = VidXPSettings(_env_file=None) + + self.assertIsNone(settings.slm_base_url) + self.assertIsNone(settings.slm_model) + + with ( + patch.dict( + os.environ, + { + "VIDXP_HTTP_AUTH_MODE": "static", + "VIDXP_HTTP_STATIC_BEARER_TOKEN": "", + }, + clear=True, + ), + self.assertRaises(ValidationError), + ): + VidXPSettings(_env_file=None) + + def test_auto_runtime_remains_cpu_until_acceleration_parity_is_enabled(self): + with patch( + "vidxp.runtime._torch_accelerators", + return_value=(True, True), + ): + profile = resolve_backends("auto") + + self.assertEqual(profile.torch_device, "cpu") + self.assertEqual(profile.transcription_device, "cpu") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_ollama_query.py b/tests/test_ollama_query.py new file mode 100644 index 0000000..61cf331 --- /dev/null +++ b/tests/test_ollama_query.py @@ -0,0 +1,136 @@ +import asyncio +import json +import unittest + +import httpx + +from vidxp.application_models import ( + DraftAnswer, + MomentEvidence, + QueryPlanningRequest, + QuerySynthesisRequest, + SearchHit, +) +from vidxp.infrastructure.ollama_query import OllamaQueryModel + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" +EVIDENCE_ID = "a" * 64 + + +class OllamaQueryModelTests(unittest.TestCase): + def test_structured_planning_and_synthesis_use_the_provider_contract(self): + outputs = [ + { + "steps": [ + { + "kind": "search_moments", + "modality": "dialogue", + "query": "taxi arrival", + } + ] + }, + { + "claims": [ + { + "text": "The taxi arrived.", + "evidence_ids": [EVIDENCE_ID], + } + ] + }, + ] + requests: list[dict[str, object]] = [] + + def handler(request: httpx.Request) -> httpx.Response: + requests.append(json.loads(request.content)) + return httpx.Response( + 200, + request=request, + json={ + "id": f"chatcmpl-{len(requests)}", + "object": "chat.completion", + "created": 1, + "model": "contract-model", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": json.dumps(outputs.pop(0)), + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 10, + "total_tokens": 20, + }, + }, + ) + + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + model = OllamaQueryModel( + base_url="http://ollama.test/v1", + model_name="contract-model", + timeout_seconds=2, + output_retries=0, + http_client=client, + ) + hit = SearchHit( + rank=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=1, + end=2, + score=-0.1, + raw_distance=0.1, + modality="dialogue", + source_id="dialogue:1", + metadata={"text": "the taxi arrived"}, + ) + evidence = MomentEvidence( + evidence_id=EVIDENCE_ID, + snapshot_id=SNAPSHOT_ID, + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + modality="dialogue", + source_id="dialogue:1", + start=1, + end=2, + display_text="the taxi arrived", + hit=hit, + ) + try: + plan = model.plan( + QueryPlanningRequest( + question="When did the taxi arrive?", + allowed_modalities=("dialogue",), + ) + ) + answer = model.synthesize( + QuerySynthesisRequest( + question="When did the taxi arrive?", + evidence=(evidence,), + ) + ) + finally: + asyncio.run(client.aclose()) + + self.assertEqual(plan.steps[0].query, "taxi arrival") + self.assertIsInstance(answer, DraftAnswer) + self.assertEqual(answer.claims[0].evidence_ids, (EVIDENCE_ID,)) + self.assertEqual(len(requests), 2) + for request in requests: + self.assertEqual( + request["response_format"]["type"], + "json_schema", + ) + self.assertEqual(request["model"], "contract-model") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 5338ddc..75b12be 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -1,13 +1,101 @@ +import json import unittest from pathlib import Path +import tomllib -from vidxp.capabilities.registry import CAPABILITIES +from packaging.requirements import Requirement +from packaging.version import Version + +from vidxp.capabilities.registry import create_capability_registry ROOT = Path(__file__).resolve().parents[1] class PackagingTests(unittest.TestCase): + def test_canonical_icon_is_packaged_and_desktop_derivatives_are_wired(self): + icon = ROOT / "docs" / "images" / "logo.png" + self.assertTrue(icon.is_file()) + self.assertTrue(icon.read_bytes().startswith(b"\x89PNG\r\n\x1a\n")) + + self.assertIn( + "include docs/images/logo.png", + (ROOT / "MANIFEST.in").read_text(encoding="utf-8"), + ) + project = tomllib.loads( + (ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + self.assertEqual( + project["project"]["urls"]["Homepage"], + "https://github.com/grayhatdevelopers/vidxp", + ) + self.assertIn( + "./docs/images/logo.png", + (ROOT / "README.md").read_text(encoding="utf-8"), + ) + build_hook = (ROOT / "setup.py").read_text(encoding="utf-8") + self.assertIn('"docs" / "images" / "logo.png"', build_hook) + self.assertIn('"vidxp" / "assets" / "icon.png"', build_hook) + + package = json.loads( + (ROOT / "desktop" / "package.json").read_text(encoding="utf-8") + ) + self.assertEqual( + package["scripts"]["sync:branding"], + "node scripts/sync-branding.mjs", + ) + self.assertEqual( + package["scripts"]["predesktop:dev"], + "npm run sync:branding", + ) + self.assertEqual( + package["scripts"]["predesktop:build"], + "npm run icons", + ) + self.assertEqual( + package["scripts"]["icons"], + ( + "npm run sync:branding && " + "tauri icon ../docs/images/logo.png " + "--output src-tauri/icons" + ), + ) + sync_script = ( + ROOT / "desktop" / "scripts" / "sync-branding.mjs" + ).read_text(encoding="utf-8") + self.assertIn("../docs/images/logo.png", sync_script) + self.assertIn("web/icon.png", sync_script) + self.assertIn( + 'href="icon.png"', + (ROOT / "desktop" / "web" / "index.html").read_text( + encoding="utf-8" + ), + ) + + def test_cpu_lock_uses_explicit_pytorch_index_without_cuda_packages(self): + lock = (ROOT / "uv.lock").read_text(encoding="utf-8") + project = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + + self.assertIn("https://download.pytorch.org/whl/cpu", lock) + self.assertNotIn('name = "nvidia-', lock) + self.assertIn('explicit = true', project) + self.assertIn("sys_platform == 'linux'", project) + self.assertIn("sys_platform == 'win32'", project) + + def test_publishable_metadata_contains_no_direct_url_requirements(self): + project = tomllib.loads( + (ROOT / "pyproject.toml").read_text(encoding="utf-8") + )["project"] + requirements = list(project["dependencies"]) + requirements.extend( + line.strip() + for path in (ROOT / "src" / "vidxp").rglob("requirements.txt") + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + ) + for value in requirements: + self.assertIsNone(Requirement(value).url, value) + def test_capability_extras_read_capability_owned_requirements(self): pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") storage = "src/vidxp/requirements/storage.txt" @@ -16,7 +104,7 @@ def test_capability_extras_read_capability_owned_requirements(self): 1, )[0] - for capability in CAPABILITIES.values(): + for capability in create_capability_registry().definitions.values(): extra_block = pyproject.split( f"{capability.extra} = {{ file = [", 1, @@ -36,13 +124,26 @@ def test_capability_extras_read_capability_owned_requirements(self): self.assertIn(f'"{relative}"', all_block) self.assertIn(f'storage = {{ file = ["{storage}"] }}', pyproject) - self.assertEqual( - sum( - line.strip() == "chromadb" - for path in (ROOT / "src" / "vidxp").rglob("*.txt") + chroma_contracts = { + path.relative_to(ROOT).as_posix() + for path in (ROOT / "src" / "vidxp").rglob("*.txt") + if any( + line.strip().startswith("chromadb") for line in path.read_text(encoding="utf-8").splitlines() - ), - 1, + ) + } + self.assertEqual( + chroma_contracts, + { + "src/vidxp/requirements/storage.txt", + "src/vidxp/requirements/server-storage.txt", + }, + ) + self.assertIn( + "chromadb-client", + ( + ROOT / "src" / "vidxp" / "requirements" / "server-storage.txt" + ).read_text(encoding="utf-8"), ) self.assertNotIn("benchmarks/requirements.txt", all_block) self.assertNotIn("requirements/frontend.txt", all_block) @@ -56,19 +157,188 @@ def test_base_dependencies_exclude_capability_runtimes(self): for distribution in ( "chromadb", - "face-recognition", - "moviepy", + "faster-whisper", "numpy", - "opencv-python", + "opencv-python-headless", "sentence-transformers", "torch", - "whisperx", - "clip-anytorch", + "transformers", + "pooch", "streamlit", "srt", ): self.assertNotIn(distribution, base_dependencies) + def test_install_profiles_keep_server_free_of_ml_dependencies(self): + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + self.assertIn("local-worker = { file = [", pyproject) + self.assertIn("mcp = { file = [", pyproject) + self.assertIn("server = { file = [", pyproject) + self.assertIn( + '"src/vidxp/requirements/mcp.txt"', + pyproject, + ) + server = ( + ROOT / "src" / "vidxp" / "requirements" / "server.txt" + ).read_text(encoding="utf-8") + for distribution in ( + "chromadb", + "faster-whisper", + "opencv-python-headless", + "sentence-transformers", + "torch", + "transformers", + ): + self.assertNotIn(distribution, server) + for requirement in ( + "asgi-correlation-id>=5.0.1,<6", + "fastapi>=0.140.13,<0.141", + "pyjwt[crypto]>=2.13,<3", + "python-multipart>=0.0.32,<0.1", + "uvicorn[standard]>=0.51,<0.52", + ): + self.assertIn(requirement, server) + self.assertNotIn("fastapi-mcp", server) + self.assertNotIn("\nmcp", server) + self.assertNotIn("httpx2", server) + mcp_requirements = ( + ROOT / "src" / "vidxp" / "requirements" / "mcp.txt" + ).read_text(encoding="utf-8") + self.assertIn("mcp>=2.0,<3", mcp_requirements) + self.assertNotIn("fastmcp", mcp_requirements) + self.assertNotIn("fastapi-mcp", mcp_requirements) + slm_requirements = ( + ROOT / "src" / "vidxp" / "requirements" / "slm.txt" + ).read_text(encoding="utf-8") + self.assertIn( + "pydantic-ai-slim[openai]>=2.13,<3", + slm_requirements, + ) + self.assertNotIn("pydantic-ai", server) + test_requirements = ( + ROOT / "src" / "vidxp" / "requirements" / "test.txt" + ).read_text(encoding="utf-8") + self.assertIn("httpx>=0.28.1,<0.29", test_requirements) + self.assertNotIn("httpx2", test_requirements) + + def test_optional_ollama_profile_never_pulls_a_model_implicitly(self): + compose = (ROOT / "compose.coolify.yaml").read_text( + encoding="utf-8" + ) + + self.assertIn( + "ollama/ollama:0.32.5@sha256:" + "4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131", + compose, + ) + self.assertIn('profiles: ["slm"]', compose) + self.assertIn( + "VIDXP_SLM_BASE_URL: ${VIDXP_SLM_BASE_URL:-}", + compose, + ) + self.assertIn( + "VIDXP_SLM_MODEL: ${VIDXP_SLM_MODEL:-}", + compose, + ) + self.assertNotIn("ollama pull", compose.lower()) + + def test_desktop_manifest_matches_published_package_contract(self): + project = tomllib.loads( + (ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + self.assertEqual( + project["project"]["scripts"]["vidxp"], + "vidxp.entrypoint:main", + ) + manifest = json.loads( + (ROOT / "desktop" / "runtime-manifest.json").read_text( + encoding="utf-8" + ) + ) + package = json.loads( + (ROOT / "desktop" / "package.json").read_text(encoding="utf-8") + ) + tauri = json.loads( + ( + ROOT + / "desktop" + / "src-tauri" + / "tauri.conf.json" + ).read_text(encoding="utf-8") + ) + cargo = tomllib.loads( + ( + ROOT / "desktop" / "src-tauri" / "Cargo.toml" + ).read_text(encoding="utf-8") + ) + + self.assertEqual(manifest["package_name"], project["project"]["name"]) + self.assertEqual( + Version(manifest["package_version"]), + Version(project["project"]["version"]), + ) + self.assertEqual(package["version"], manifest["desktop_version"]) + self.assertEqual(tauri["version"], manifest["desktop_version"]) + self.assertFalse(tauri["app"]["windows"][0]["visible"]) + self.assertEqual( + tauri["bundle"]["windows"]["nsis"]["installerHooks"], + "nsis-hooks.nsh", + ) + nsis_hooks = ( + ROOT / "desktop" / "src-tauri" / "nsis-hooks.nsh" + ).read_text(encoding="utf-8") + self.assertIn( + r"$LOCALAPPDATA\Programs\${PRODUCTNAME}", + nsis_hooks, + ) + self.assertIn( + "tray-icon", + cargo["dependencies"]["tauri"]["features"], + ) + self.assertEqual( + Version(manifest["desktop_version"]), + Version(manifest["package_version"]), + ) + self.assertEqual(manifest["python_version"], "3.14.6") + self.assertEqual(manifest["uv_version"], "0.12.0") + sidecars = json.loads( + (ROOT / "desktop" / "sidecars.json").read_text(encoding="utf-8") + ) + self.assertEqual(sidecars["uv_version"], manifest["uv_version"]) + self.assertEqual( + set(sidecars["targets"]), + { + "x86_64-pc-windows-msvc", + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + }, + ) + for target in sidecars["targets"].values(): + self.assertRegex(target["sha256"], r"^[a-f0-9]{64}$") + dynamic_extras = project["tool"]["setuptools"]["dynamic"][ + "optional-dependencies" + ] + self.assertEqual(set(manifest["surfaces"]), {"browser"}) + self.assertTrue(manifest["surfaces"]["browser"]["default"]) + for surface in manifest["surfaces"].values(): + self.assertIn(surface["extra"], dynamic_extras) + for capability in manifest["capabilities"].values(): + self.assertIn(capability["extra"], dynamic_extras) + self.assertEqual( + manifest["media_runtime"]["strategy"], + "system", + ) + + def test_windows_release_binary_uses_the_gui_subsystem(self): + main = ( + ROOT / "desktop" / "src-tauri" / "src" / "main.rs" + ).read_text(encoding="utf-8") + + self.assertIn( + '#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]', + main, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_query_service.py b/tests/test_query_service.py new file mode 100644 index 0000000..a57029d --- /dev/null +++ b/tests/test_query_service.py @@ -0,0 +1,377 @@ +import unittest + +from vidxp.application_models import ( + DraftAnswer, + DraftClaim, + FusedSearchResult, + FusionProvenance, + IndexSnapshotReference, + QueryAnswerMode, + QueryModelIdentity, + QueryPlan, + QueryVideoCommand, + SearchHit, + SearchMomentsPlanStep, + SearchResult, +) +from vidxp.ports import QueryProviderError +from vidxp.query_service import GroundedQueryService +from vidxp.search_fusion import fuse_search_results + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" +SNAPSHOT_SHA256 = "a" * 64 + + +class FakeQueryModel: + identity = QueryModelIdentity(provider="ollama", model="test") + + def __init__(self, plan, answer=None): + self.proposed_plan = plan + self.proposed_answer = answer + + def plan(self, _request): + if isinstance(self.proposed_plan, Exception): + raise self.proposed_plan + return self.proposed_plan + + def synthesize(self, _request): + if isinstance(self.proposed_answer, Exception): + raise self.proposed_answer + return self.proposed_answer + + +def result(*, text: str | None = None, modality: str = "scene"): + metadata = {"text": text} if text is not None else {} + return SearchResult( + query_id=f"{modality}:q", + query="taxi", + modality=modality, + hits=( + SearchHit( + rank=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=1, + end=2, + score=-0.1, + raw_distance=0.1, + modality=modality, + source_id=f"{modality}:1", + metadata=metadata, + ), + ), + ) + + +def fused( + modality: str = "scene", + atomic: SearchResult | None = None, +): + if atomic is not None: + return fuse_search_results( + query=atomic.query, + requested_modalities=(modality,), + results=(atomic,), + ) + return FusedSearchResult( + query_id="fused:q", + query="taxi", + modalities=(modality,), + fusion=FusionProvenance( + requested_modalities=(modality,), + searched_modalities=(modality,), + ), + ) + + +class GroundedQueryServiceTests(unittest.TestCase): + def setUp(self): + self.command = QueryVideoCommand(question="Where is the taxi?") + self.snapshot = IndexSnapshotReference( + snapshot_id=SNAPSHOT_ID, + snapshot_sha256=SNAPSHOT_SHA256, + ) + + def test_invalid_model_plan_falls_back_to_complete_closed_plan(self): + model = FakeQueryModel( + QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="scene", + query="taxi", + ), + ) + ) + ) + service = GroundedQueryService(model) + + plan, reason = service.plan( + self.command, + search_modalities=("scene", "dialogue"), + actor_overview=False, + ) + + self.assertEqual(reason, "query_plan_rejected") + self.assertEqual( + [step.modality for step in plan.steps], + ["scene", "dialogue"], + ) + + def test_provider_failure_uses_deterministic_retrieval_plan(self): + service = GroundedQueryService( + FakeQueryModel(QueryProviderError("offline")) + ) + + plan, reason = service.plan( + self.command, + search_modalities=("scene",), + actor_overview=False, + ) + + self.assertEqual(reason, "query_model_unavailable") + self.assertEqual(plan.steps[0].query, self.command.question) + + def test_scene_only_evidence_never_becomes_a_generated_claim(self): + service = GroundedQueryService( + FakeQueryModel( + QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="scene", + query="taxi", + ), + ) + ), + DraftAnswer( + claims=( + DraftClaim( + text="A taxi is visible.", + evidence_ids=("b" * 64,), + ), + ) + ), + ) + ) + atomic = result() + fused_result = fused(atomic=atomic) + evidence = service.evidence( + snapshot=self.snapshot, + fused=fused_result, + actors=(), + ) + + answer = service.answer( + self.command, + plan=service.plan( + self.command, + search_modalities=("scene",), + actor_overview=False, + )[0], + planning_fallback=None, + evidence=evidence, + fused=fused_result, + ) + + self.assertEqual(answer.mode, QueryAnswerMode.evidence_only) + self.assertEqual(answer.fallback_reason, "no_textual_evidence") + self.assertFalse(answer.claims) + + def test_unknown_citation_is_rejected(self): + plan = QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="dialogue", + query="taxi", + ), + ) + ) + service = GroundedQueryService( + FakeQueryModel( + plan, + DraftAnswer( + claims=( + DraftClaim( + text="Someone mentions a taxi.", + evidence_ids=("b" * 64,), + ), + ) + ), + ) + ) + atomic = result(text="the taxi arrived", modality="dialogue") + fused_result = fused("dialogue", atomic) + evidence = service.evidence( + snapshot=self.snapshot, + fused=fused_result, + actors=(), + ) + + answer = service.answer( + self.command, + plan=plan, + planning_fallback=None, + evidence=evidence, + fused=fused_result, + ) + + self.assertEqual(answer.mode, QueryAnswerMode.evidence_only) + self.assertEqual(answer.fallback_reason, "query_citations_rejected") + + def test_valid_textual_citation_is_reconstructed_by_the_application(self): + plan = QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="dialogue", + query="taxi", + ), + ) + ) + atomic = result(text="the taxi arrived", modality="dialogue") + service = GroundedQueryService() + fused_result = fused("dialogue", atomic) + evidence = service.evidence( + snapshot=self.snapshot, + fused=fused_result, + actors=(), + ) + service.model = FakeQueryModel( + plan, + DraftAnswer( + claims=( + DraftClaim( + text="The taxi arrived.", + evidence_ids=(evidence[0].evidence_id,), + ), + ) + ), + ) + + answer = service.answer( + self.command, + plan=plan, + planning_fallback=None, + evidence=evidence, + fused=fused_result, + ) + + self.assertEqual(answer.mode, QueryAnswerMode.generated) + self.assertEqual( + answer.claims[0].evidence_ids, + (evidence[0].evidence_id,), + ) + + def test_generated_answer_preserves_rejected_plan_provenance(self): + atomic = result(text="the taxi arrived", modality="dialogue") + fused_result = fused("dialogue", atomic) + service = GroundedQueryService( + FakeQueryModel( + QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="dialogue", + query="taxi", + ), + ) + ) + ) + ) + evidence = service.evidence( + snapshot=self.snapshot, + fused=fused_result, + actors=(), + ) + service.model.proposed_answer = DraftAnswer( + claims=( + DraftClaim( + text="The taxi arrived.", + evidence_ids=(evidence[0].evidence_id,), + ), + ) + ) + + answer = service.answer( + self.command, + plan=service.model.proposed_plan, + planning_fallback="query_plan_rejected", + evidence=evidence, + fused=fused_result, + ) + + self.assertEqual(answer.mode, QueryAnswerMode.generated) + self.assertEqual(answer.fallback_reason, "query_plan_rejected") + self.assertEqual(answer.model, service.model.identity) + + def test_configured_model_identity_is_retained_without_evidence(self): + plan = QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="scene", + query="taxi", + ), + ) + ) + service = GroundedQueryService(FakeQueryModel(plan)) + + answer = service.answer( + self.command, + plan=plan, + planning_fallback=None, + evidence=(), + fused=fused(), + ) + + self.assertEqual(answer.mode, QueryAnswerMode.no_evidence) + self.assertEqual(answer.model, service.model.identity) + + def test_evidence_is_bounded_to_retained_fused_moments(self): + atomic = SearchResult( + query_id="dialogue:many", + query="taxi", + modality="dialogue", + hits=tuple( + SearchHit( + rank=index + 1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=float(index * 2), + end=float(index * 2 + 1), + score=-float(index + 1), + raw_distance=float(index + 1), + modality="dialogue", + source_id=f"dialogue:{index}", + metadata={"text": f"line {index}"}, + ) + for index in range(201) + ), + ) + fused_result = fuse_search_results( + query="taxi", + requested_modalities=("dialogue",), + results=(atomic,), + top_k=201, + ) + + evidence = GroundedQueryService.evidence( + snapshot=self.snapshot, + fused=fused_result, + actors=(), + ) + + self.assertEqual(len(evidence), 200) + retained_sources = { + hit.source_id + for moment in fused_result.moments + for hit in moment.hits + } + self.assertTrue( + all(item.source_id in retained_sources for item in evidence) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_read_job_planner.py b/tests/test_read_job_planner.py new file mode 100644 index 0000000..bb12274 --- /dev/null +++ b/tests/test_read_job_planner.py @@ -0,0 +1,73 @@ +import unittest +from unittest.mock import Mock + +from vidxp.application_models import ( + CreateActorOverlayCommand, + QueryVideoCommand, + SearchCommand, +) +from vidxp.core.contracts import IndexConfig +from vidxp.read_job_planner import LocalReadJobPlanner +from vidxp.repository_layout import RepositoryLayout + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" + + +class LocalReadJobPlannerTests(unittest.TestCase): + def setUp(self): + self.layout = RepositoryLayout(root="unused") + self.config = IndexConfig.local( + video_id=MEDIA_ID, + enabled_modalities=("scene", "actor"), + snapshot_id=SNAPSHOT_ID, + snapshot_sha256="a" * 64, + storage_directory=self.layout.index_store, + collection_names={"scene": "scene", "actor": "actor"}, + ) + self.index = Mock() + self.index.active_config.return_value = self.config + self.planner = LocalReadJobPlanner( + layout=self.layout, + index=self.index, + ) + + def test_search_job_carries_only_the_logical_snapshot_reference(self): + request = self.planner.plan_search( + SearchCommand(modalities=("scene",), query="a taxi"), + ) + + self.assertEqual(request.snapshot.snapshot_id, SNAPSHOT_ID) + self.assertEqual(request.snapshot.snapshot_sha256, "a" * 64) + self.assertNotIn( + "storage_directory", + request.model_dump(mode="json"), + ) + self.index.open_store.assert_not_called() + + def test_actor_job_pins_snapshot_without_opening_vector_storage(self): + request = self.planner.plan_actor_overlay( + CreateActorOverlayCommand(cluster_id="actor-1"), + ) + + self.assertEqual(request.snapshot.snapshot_id, SNAPSHOT_ID) + self.index.open_store.assert_not_called() + + def test_query_job_carries_the_same_logical_snapshot_reference(self): + request = self.planner.plan_query( + QueryVideoCommand( + question="What happens?", + modalities=("scene",), + ) + ) + + self.assertEqual(request.command.modalities, ("scene",)) + self.assertEqual(request.snapshot.snapshot_id, SNAPSHOT_ID) + self.assertEqual(request.snapshot.snapshot_sha256, "a" * 64) + self.index.open_store.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_repositories.py b/tests/test_repositories.py index cbf80e7..1a24b72 100644 --- a/tests/test_repositories.py +++ b/tests/test_repositories.py @@ -5,6 +5,7 @@ from tempfile import TemporaryDirectory from unittest.mock import patch +from vidxp.app_paths import default_repository_directory from vidxp.repositories import ( DEFAULT_REPOSITORY_NAME, RepositoryConfigError, @@ -23,7 +24,37 @@ def test_missing_registry_exposes_non_persistent_default(self): self.assertEqual(repositories[0].name, DEFAULT_REPOSITORY_NAME) self.assertFalse(repositories[0].configured) - self.assertEqual(selected.index_directory, Path("chroma_data")) + self.assertEqual( + selected.index_directory, + default_repository_directory(), + ) + self.assertTrue(selected.index_directory.is_absolute()) + + def test_data_directory_controls_the_implicit_default_repository(self): + with TemporaryDirectory() as directory: + root = Path(directory) + registry_path = root / "repos.json" + data_directory = root / "application-data" + unrelated_working_directory = root / "working-directory" + unrelated_working_directory.mkdir() + original_working_directory = Path.cwd() + try: + os.chdir(unrelated_working_directory) + with patch.dict( + os.environ, + {"VIDXP_DATA_DIR": str(data_directory)}, + clear=False, + ): + _, selected = resolve_repository( + registry_path=registry_path, + ) + finally: + os.chdir(original_working_directory) + + self.assertEqual( + selected.index_directory, + data_directory / "repositories" / "default", + ) def test_add_use_replace_and_remove_round_trip(self): with TemporaryDirectory() as directory: @@ -109,6 +140,26 @@ def test_explicit_and_environment_overrides_have_stable_precedence(self): self.assertEqual(selected.index_directory, Path("explicit-index")) self.assertEqual(selected.device, "cuda") + def test_explicit_data_directory_precedes_environment_default(self): + with TemporaryDirectory() as directory: + root = Path(directory) + environment_root = root / "environment-data" + explicit_root = root / "explicit-data" + with patch.dict( + os.environ, + {"VIDXP_DATA_DIR": str(environment_root)}, + clear=False, + ): + _, selected = resolve_repository( + registry_path=root / "repos.json", + data_directory=explicit_root, + ) + + self.assertEqual( + selected.index_directory, + explicit_root / "repositories" / "default", + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_runner.py b/tests/test_runner.py index 00db29a..852d67e 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -5,23 +5,23 @@ from unittest.mock import Mock, patch from vidxp.core.contracts import ( - INDEX_SCHEMA_VERSION, IndexCancelledError, IndexConfig, - IndexSchemaError, VideoSource, ) -from vidxp.core.manifest import COMPLETION_FILE +from vidxp.core.manifest import COMPLETION_FILE, ManifestStore from vidxp.capabilities.contracts import CapabilityIndexResult -from vidxp.capabilities.dialogue.config import dialogue_config +from vidxp.capabilities.dialogue.specs import FASTER_WHISPER_MODEL from vidxp.core.runner import ( _RunLock, - index_video, + index_video as _index_video, indexing_in_progress, - local_config_from_status, - run_index, + run_index as _run_index, ) +from vidxp.capabilities.registry import create_capability_registry from vidxp.index_state import IndexingInProgressError +from vidxp.runtime import ModelRuntime +from vidxp.settings import VidXPSettings EXECUTION_STATE = { @@ -34,6 +34,50 @@ } +def _dependencies(config): + return { + "registry": create_capability_registry(), + "runtime": ModelRuntime( + VidXPSettings( + repository_root=config.run_directory, + runtime_backend=config.device, + ) + ), + } + + +def run_index(sources, config, **options): + dependencies = _dependencies(config) + options.setdefault("registry", dependencies["registry"]) + options.setdefault("runtime", dependencies["runtime"]) + options.setdefault("storage", FakeStorage()) + options.setdefault( + "manifest_store", + ManifestStore( + config, + registry=options["registry"], + runtime=options["runtime"], + ), + ) + return _run_index(sources, config, **options) + + +def index_video(path, *args, config, **options): + dependencies = _dependencies(config) + options.setdefault("registry", dependencies["registry"]) + options.setdefault("runtime", dependencies["runtime"]) + options.setdefault("storage", FakeStorage()) + options.setdefault( + "manifest_store", + ManifestStore( + config, + registry=options["registry"], + runtime=options["runtime"], + ), + ) + return _index_video(path, *args, config=config, **options) + + def visual_result(summary, timings=None): normalized = dict(summary) scene_frames = int(normalized.get("scene_frames", 0)) @@ -63,6 +107,17 @@ def clear(self, modalities=None): def delete_video(self, modality, video_id): self.deleted.append((modality, video_id)) + def delete_records( + self, + modality, + *, + video_id, + filters=None, + ): + self.deleted.append( + (modality, video_id, dict(filters or {})) + ) + def size_bytes(self): return self.size @@ -77,16 +132,6 @@ def _config(self, root, modalities=("scene",)): output_root=root, ) - def test_local_config_rejects_an_older_index_schema(self): - status = { - "summary": { - "index_schema_version": INDEX_SCHEMA_VERSION - 1, - } - } - - with self.assertRaisesRegex(IndexSchemaError, "Re-index"): - local_config_from_status(status) - def test_two_videos_complete_one_isolated_resumable_run(self): with TemporaryDirectory() as directory: root = Path(directory) @@ -134,7 +179,10 @@ def scene_indexer(source, *, config, **_): ) self.assertEqual(resumed["state"], "complete") self.assertEqual(manifest["git"]["commit"], "abc123") - self.assertEqual(manifest["index_size_bytes"], 321) + self.assertEqual( + manifest["store_size_bytes_at_commit"], + 321, + ) self.assertEqual(manifest["processed_frames"], 4) self.assertTrue((config.run_directory / COMPLETION_FILE).is_file()) self.assertEqual( @@ -142,6 +190,66 @@ def scene_indexer(source, *, config, **_): [("scene", "video-1"), ("scene", "video-2")], ) + def test_generation_cleanup_is_scoped_to_each_video(self): + with TemporaryDirectory() as directory: + root = Path(directory) + first = root / "first.mp4" + second = root / "second.mp4" + first.write_bytes(b"first") + second.write_bytes(b"second") + config = IndexConfig( + dataset="sample", + split="test", + run_id="run-1", + enabled_modalities=("scene",), + output_root=root, + generation_id="generation-1", + ) + storage = FakeStorage() + + with ( + patch("vidxp.core.runner.require_dependencies"), + patch( + "vidxp.capabilities.visual.index_visuals", + return_value=visual_result( + { + "scene_frames": 1, + "decoded_frames": 1, + "duration": 1.0, + "fps": 1.0, + } + ), + ), + patch( + "vidxp.core.manifest.execution_state", + return_value=EXECUTION_STATE, + ), + ): + run_index( + [ + VideoSource(video_id="video-1", path=first), + VideoSource(video_id="video-2", path=second), + ], + config, + storage=storage, + ) + + self.assertEqual( + storage.deleted, + [ + ( + "scene", + "video-1", + {"generation_id": "generation-1"}, + ), + ( + "scene", + "video-2", + {"generation_id": "generation-1"}, + ), + ], + ) + def test_scene_and_actor_are_dispatched_as_one_visual_pipeline(self): with TemporaryDirectory() as directory: path = Path(directory) / "video.mp4" @@ -281,10 +389,12 @@ def test_transcript_only_run_does_not_request_transcription_dependencies(self): ): run_index([source], config, storage=FakeStorage()) - dependency_check.assert_called_once_with( - ("dialogue",), - source=source, + dependency_check.assert_called_once() + self.assertEqual( + dependency_check.call_args.args, + (("dialogue",),), ) + self.assertIs(dependency_check.call_args.kwargs["source"], source) def test_manifest_adds_transcription_model_when_run_later_needs_it(self): with TemporaryDirectory() as directory: @@ -321,10 +431,10 @@ def test_manifest_adds_transcription_model_when_run_later_needs_it(self): ) self.assertNotIn("transcription", first["models"]) - self.assertEqual( - second["models"]["transcription"], - dialogue_config(config).whisper_model, - ) + self.assertEqual( + second["models"]["transcription"]["model"], + FASTER_WHISPER_MODEL.model_id, + ) def test_changed_input_is_not_silently_accepted_by_checkpoint(self): with TemporaryDirectory() as directory: @@ -571,22 +681,12 @@ def test_local_index_hash_is_reused_by_run_index(self): "vidxp.core.runner.run_index", return_value=manifest, ) as run, - patch( - "vidxp.core.runner.write_index_status", - ) as write_status, ): index_video(str(path), config=config) hash_source.assert_called_once() indexed_source = run.call_args.args[0][0] self.assertEqual(indexed_source.checksum, checksum) - self.assertTrue( - all( - call.kwargs["index_directory"] - == config.index_directory - for call in write_status.call_args_list - ) - ) def test_manifest_and_timing_files_are_valid_json(self): with TemporaryDirectory() as directory: diff --git a/tests/test_search.py b/tests/test_search.py index 3b10003..be7c58a 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -3,8 +3,11 @@ from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import patch +from unittest.mock import Mock +import numpy as np from vidxp.capabilities.dialogue.operations import search_dialogue +from vidxp.capabilities.dialogue.operations import dialogue_embedding from vidxp.capabilities.schemas import SearchResult from vidxp.capabilities.search import ( distance_to_score, @@ -12,6 +15,12 @@ stable_query_id, ) from vidxp.core.contracts import IndexConfig, IndexSchemaError +from vidxp.runtime import ModelRuntime +from vidxp.settings import VidXPSettings + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" class FakeStorage: @@ -24,7 +33,7 @@ def query(self, modality, embedding, **options): return list(self.rows) -def dialogue_row(source_id, distance, video_id="video-1"): +def dialogue_row(source_id, distance, video_id=MEDIA_ID): return { "source_id": source_id, "raw_distance": distance, @@ -33,6 +42,7 @@ def dialogue_row(source_id, distance, video_id="video-1"): "split": "test", "run_id": "run-1", "video_id": video_id, + "generation_id": GENERATION_ID, "source_id": source_id, "start": 1.0, "end": 2.0, @@ -51,6 +61,12 @@ def setUp(self): run_id="run-1", enabled_modalities=("dialogue",), ) + self.runtime = ModelRuntime( + VidXPSettings( + repository_root="unused", + runtime_backend="cpu", + ) + ) def test_top_k_filter_order_distance_and_score_are_preserved(self): storage = FakeStorage( @@ -67,8 +83,9 @@ def test_top_k_filter_order_distance_and_score_are_preserved(self): result = search_dialogue( "fresh bread", config=self.config, + runtime=self.runtime, top_k=3, - video_id="video-1", + video_id=MEDIA_ID, query_id="query-7", storage=storage, ) @@ -84,13 +101,33 @@ def test_top_k_filter_order_distance_and_score_are_preserved(self): self.assertEqual([hit.rank for hit in result.hits], [1, 2, 3]) self.assertEqual(result.hits[0].raw_distance, 0.1) self.assertEqual(result.hits[0].score, -0.1) + self.assertEqual( + result.hits[0].metadata, + {"text": "fresh bread", "phrase_id": 3}, + ) self.assertEqual(storage.calls[0][2]["top_k"], 3) - self.assertEqual(storage.calls[0][2]["video_id"], "video-1") + self.assertEqual(storage.calls[0][2]["video_id"], MEDIA_ID) def test_score_is_strictly_monotonic_and_not_a_probability(self): self.assertGreater(distance_to_score(0.1), distance_to_score(0.2)) self.assertEqual(distance_to_score(2.5), -2.5) + def test_dialogue_query_uses_model_owned_query_prompt(self): + encoder = Mock() + encoder.encode_query.return_value = np.asarray([[0.5, 0.25]]) + with patch( + "vidxp.capabilities.dialogue.operations.get_embedder", + return_value=encoder, + ): + vector = dialogue_embedding("fresh bread", self.config, self.runtime) + + self.assertEqual(vector, [0.5, 0.25]) + encoder.encode_query.assert_called_once_with( + ["fresh bread"], + convert_to_numpy=True, + normalize_embeddings=True, + ) + def test_generated_query_ids_are_scoped_to_the_benchmark_run(self): first = stable_query_id("fresh bread", "dialogue", self.config) second = stable_query_id( @@ -111,6 +148,7 @@ def test_nonpositive_top_k_is_rejected_before_querying(self): search_dialogue( "query", config=self.config, + runtime=self.runtime, top_k=0, storage=FakeStorage([]), ) @@ -135,6 +173,7 @@ def test_old_metadata_requires_an_explicit_reindex(self): search_dialogue( "query", config=self.config, + runtime=self.runtime, storage=storage, ) diff --git a/tests/test_search_fusion.py b/tests/test_search_fusion.py new file mode 100644 index 0000000..6d83e53 --- /dev/null +++ b/tests/test_search_fusion.py @@ -0,0 +1,114 @@ +import unittest + +from vidxp.application_models import SearchHit, SearchResult +from vidxp.search_fusion import RRF_RANK_CONSTANT, fuse_search_results + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" + + +def hit( + modality: str, + rank: int, + start: float, + end: float, + source_id: str, +) -> SearchHit: + return SearchHit( + rank=rank, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=start, + end=end, + score=-float(rank), + raw_distance=float(rank), + modality=modality, + source_id=source_id, + ) + + +class SearchFusionTests(unittest.TestCase): + def test_rrf_counts_only_the_best_rank_per_modality_in_a_moment(self): + scene = SearchResult( + query_id="scene:q", + query="taxi", + modality="scene", + hits=( + hit("scene", 1, 1, 3, "scene:1"), + hit("scene", 2, 2, 4, "scene:2"), + ), + ) + dialogue = SearchResult( + query_id="dialogue:q", + query="taxi", + modality="dialogue", + hits=(hit("dialogue", 1, 2.5, 3.5, "dialogue:1"),), + ) + + result = fuse_search_results( + query="taxi", + requested_modalities=("scene", "dialogue"), + results=(scene, dialogue), + ) + + self.assertEqual(len(result.moments), 1) + moment = result.moments[0] + self.assertAlmostEqual(moment.score, 2 / (RRF_RANK_CONSTANT + 1)) + self.assertEqual(len(moment.hits), 3) + self.assertEqual(moment.start, 1) + self.assertEqual(moment.end, 4) + + def test_result_order_does_not_change_fusion_identity_or_output(self): + scene = SearchResult( + query_id="scene:q", + query="taxi", + modality="scene", + hits=(hit("scene", 1, 1, 2, "scene:1"),), + ) + dialogue = SearchResult( + query_id="dialogue:q", + query="taxi", + modality="dialogue", + hits=(hit("dialogue", 1, 1, 2, "dialogue:1"),), + ) + arguments = { + "query": "taxi", + "requested_modalities": ("scene", "dialogue"), + } + + forward = fuse_search_results( + results=(scene, dialogue), + **arguments, + ) + reverse = fuse_search_results( + results=(dialogue, scene), + **arguments, + ) + + self.assertEqual(forward, reverse) + + def test_rewritten_atomic_query_identity_changes_fused_identity(self): + original = SearchResult( + query_id="scene:original", + query="taxi", + modality="scene", + hits=(hit("scene", 1, 1, 2, "scene:1"),), + ) + rewritten = original.model_copy( + update={"query_id": "scene:rewritten"} + ) + arguments = { + "query": "Where is the taxi?", + "requested_modalities": ("scene",), + } + + first = fuse_search_results(results=(original,), **arguments) + second = fuse_search_results(results=(rewritten,), **arguments) + + self.assertNotEqual(first.query_id, second.query_id) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..c089a17 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,121 @@ +import os +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from pydantic import ValidationError + +from vidxp.app_paths import ( + default_data_directory, + default_model_directory, + default_repository_directory, +) +from vidxp.settings import ( + ApplicationMode, + LocalExecutionSettings, + VidXPSettings, +) + + +class SettingsTests(unittest.TestCase): + def test_default_storage_is_under_the_per_user_data_directory(self): + with TemporaryDirectory() as directory: + unrelated_working_directory = Path(directory) + original_working_directory = Path.cwd() + try: + os.chdir(unrelated_working_directory) + settings = VidXPSettings() + finally: + os.chdir(original_working_directory) + + self.assertEqual(settings.data_dir, default_data_directory()) + self.assertEqual( + settings.repository_root, + default_repository_directory(settings.data_dir), + ) + self.assertEqual( + settings.model_cache, + default_model_directory(settings.data_dir), + ) + self.assertTrue(settings.data_dir.is_absolute()) + + def test_data_directory_environment_derives_storage_defaults(self): + with TemporaryDirectory() as directory: + data_directory = Path(directory) / "vidxp-data" + with patch.dict( + os.environ, + {"VIDXP_DATA_DIR": str(data_directory)}, + clear=False, + ): + settings = VidXPSettings() + + self.assertEqual(settings.data_dir, data_directory) + self.assertEqual( + settings.repository_root, + data_directory / "repositories" / "default", + ) + self.assertEqual(settings.model_cache, data_directory / "models") + + def test_explicit_storage_paths_override_derived_defaults(self): + with TemporaryDirectory() as directory: + root = Path(directory) + data_directory = root / "data" + repository_root = root / "repository" + model_cache = root / "model-cache" + with patch.dict( + os.environ, + {"VIDXP_DATA_DIR": str(root / "environment-data")}, + clear=False, + ): + settings = VidXPSettings( + data_dir=data_directory, + repository_root=repository_root, + model_cache=model_cache, + ) + + self.assertEqual(settings.data_dir, data_directory) + self.assertEqual(settings.repository_root, repository_root) + self.assertEqual(settings.model_cache, model_cache) + + def test_saved_media_runtime_paths_become_settings_defaults(self): + ffmpeg = Path("tools/ffmpeg").resolve() + ffprobe = Path("tools/ffprobe").resolve() + + def executable(name): + return str(ffmpeg if name == "ffmpeg" else ffprobe) + + with patch( + "vidxp.settings.default_media_executable", + side_effect=executable, + ): + settings = VidXPSettings() + + self.assertEqual(settings.ffmpeg_executable, str(ffmpeg)) + self.assertEqual(settings.ffprobe_executable, str(ffprobe)) + + def test_local_execution_settings_preserve_the_data_directory(self): + with TemporaryDirectory() as directory: + expected = VidXPSettings(data_dir=Path(directory) / "data") + + reconstructed = LocalExecutionSettings.from_settings( + expected + ).application_settings() + + self.assertEqual(reconstructed.data_dir, expected.data_dir) + self.assertEqual( + reconstructed.repository_root, + expected.repository_root, + ) + self.assertEqual(reconstructed.model_cache, expected.model_cache) + + def test_unimplemented_remote_mode_is_rejected_explicitly(self): + with self.assertRaisesRegex( + ValidationError, + "Remote client mode is not available", + ): + VidXPSettings(mode=ApplicationMode.remote) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_snapshots.py b/tests/test_snapshots.py new file mode 100644 index 0000000..1057dd9 --- /dev/null +++ b/tests/test_snapshots.py @@ -0,0 +1,168 @@ +import json +import unittest +from datetime import datetime, timezone +from uuid import uuid1, uuid4 + +from pydantic import ValidationError + +from vidxp.core.snapshots import ( + ACTIVE_SNAPSHOT_POINTER_SCHEMA_VERSION, + INDEX_SNAPSHOT_SCHEMA_VERSION, + ActiveSnapshotPointer, + GenerationReference, + IndexSnapshot, +) + + +SHA256 = "a" * 64 +OTHER_SHA256 = "b" * 64 + + +class SnapshotModelTests(unittest.TestCase): + def generation_reference(self, **changes): + values = { + "generation_id": uuid4().hex, + "media_id": "media-1", + "manifest_sha256": SHA256, + "input_sha256": OTHER_SHA256, + "config_fingerprint": SHA256, + "modalities": ("dialogue", "scene"), + "record_counts": {"dialogue": 3, "scene": 4}, + "store_size_bytes_at_commit": 2048, + } + values.update(changes) + return GenerationReference(**values) + + def index_snapshot(self, **changes): + reference = changes.pop("reference", self.generation_reference()) + values = { + "snapshot_id": uuid4().hex, + "created_at": datetime(2026, 7, 29, tzinfo=timezone.utc), + "config_fingerprint": SHA256, + "configuration": { + "device": "cpu", + "modalities": ["dialogue", "scene"], + "options": {"batch_size": 8}, + }, + "generations": {reference.media_id: reference}, + } + values.update(changes) + return IndexSnapshot(**values) + + def test_models_round_trip_through_json(self): + snapshot = self.index_snapshot() + pointer = ActiveSnapshotPointer( + snapshot_id=snapshot.snapshot_id, + snapshot_sha256=OTHER_SHA256, + updated_at=datetime(2026, 7, 29, 1, 2, tzinfo=timezone.utc), + ) + + reference = next(iter(snapshot.generations.values())) + self.assertEqual( + GenerationReference.model_validate_json( + reference.model_dump_json() + ), + reference, + ) + self.assertEqual( + IndexSnapshot.model_validate_json(snapshot.model_dump_json()), + snapshot, + ) + self.assertEqual( + ActiveSnapshotPointer.model_validate_json( + pointer.model_dump_json() + ), + pointer, + ) + self.assertEqual( + json.loads(snapshot.model_dump_json())["schema_version"], + INDEX_SNAPSHOT_SCHEMA_VERSION, + ) + self.assertEqual( + json.loads(pointer.model_dump_json())["schema_version"], + ACTIVE_SNAPSHOT_POINTER_SCHEMA_VERSION, + ) + + def test_models_are_strict_frozen_and_forbid_extra_fields(self): + reference = self.generation_reference() + with self.assertRaises(ValidationError): + reference.store_size_bytes_at_commit = 1 + with self.assertRaises(ValidationError): + self.generation_reference(store_size_bytes_at_commit="2048") + with self.assertRaises(ValidationError): + GenerationReference( + **reference.model_dump(), + unexpected=True, + ) + + def test_ids_must_be_lowercase_uuid4_hex(self): + invalid_ids = ( + str(uuid4()), + uuid4().hex.upper(), + uuid1().hex, + "0" * 32, + ) + for invalid_id in invalid_ids: + with self.subTest(invalid_id=invalid_id): + with self.assertRaises(ValidationError): + self.generation_reference(generation_id=invalid_id) + with self.assertRaises(ValidationError): + self.index_snapshot(snapshot_id=invalid_id) + + def test_sha256_fields_require_lowercase_64_character_hex(self): + for invalid_hash in ("a" * 63, "A" * 64, "g" * 64): + with self.subTest(invalid_hash=invalid_hash): + with self.assertRaises(ValidationError): + self.generation_reference( + manifest_sha256=invalid_hash + ) + with self.assertRaises(ValidationError): + ActiveSnapshotPointer( + snapshot_id=uuid4().hex, + snapshot_sha256=invalid_hash, + updated_at=datetime.now(timezone.utc), + ) + + def test_snapshot_rejects_mismatched_media_mapping(self): + reference = self.generation_reference(media_id="media-1") + with self.assertRaisesRegex( + ValidationError, + "mapping keys must match", + ): + self.index_snapshot( + reference=reference, + generations={"media-2": reference}, + ) + + def test_generation_counts_must_match_modalities(self): + with self.assertRaisesRegex(ValidationError, "exactly match"): + self.generation_reference(record_counts={"scene": 1}) + with self.assertRaises(ValidationError): + self.generation_reference( + record_counts={"dialogue": -1, "scene": 1} + ) + + def test_generation_store_size_may_be_unknown(self): + reference = self.generation_reference( + store_size_bytes_at_commit=None + ) + + self.assertIsNone(reference.store_size_bytes_at_commit) + + def test_snapshot_configuration_must_be_json_safe(self): + with self.assertRaises(ValidationError): + self.index_snapshot(configuration={"runtime": object()}) + + def test_timestamps_must_be_timezone_aware(self): + with self.assertRaises(ValidationError): + self.index_snapshot(created_at=datetime(2026, 7, 29)) + with self.assertRaises(ValidationError): + ActiveSnapshotPointer( + snapshot_id=uuid4().hex, + snapshot_sha256=SHA256, + updated_at=datetime(2026, 7, 29), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_storage.py b/tests/test_storage.py index b52d322..3f3b8ec 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,13 +1,28 @@ import unittest +from dataclasses import replace from pathlib import Path from tempfile import TemporaryDirectory +from unittest.mock import MagicMock + +import httpx +from chromadb.errors import ( + InvalidArgumentError, + InvalidDimensionException, + VersionMismatchError, +) from vidxp.core.contracts import ( CancellationToken, IndexConfig, StorageRecord, ) -from vidxp.core.storage import IndexStorage, directory_size, metadata_filter +from vidxp.core.storage import ( + IndexStorage, + IndexStorageUnavailableError, + SnapshotScopedIndexStore, + directory_size, + metadata_filter, +) class FakeCollection: @@ -34,6 +49,7 @@ def query(self, **options): def get(self, **options): self.get_options = options return { + "ids": ["source-3", "source-1"], "metadatas": [ { "frame_index": 3, @@ -66,6 +82,8 @@ def fake_storage(config, collection): storage.config = config storage.path = config.index_directory storage.client = FakeClient(collection) + storage._client_factory = MagicMock(remote=False) + storage._create = True storage._collections = {} storage._names = { "dialogue": "dialogue", @@ -84,6 +102,51 @@ def setUp(self): enabled_modalities=("scene",), ) + def test_remote_transport_failures_are_retryable_storage_failures(self): + storage = object.__new__(IndexStorage) + storage._client_factory = MagicMock(remote=True) + + with self.assertRaises(IndexStorageUnavailableError): + storage._call( + MagicMock(side_effect=httpx.ConnectError("offline")) + ) + + def test_remote_validation_failures_retain_their_original_type(self): + storage = object.__new__(IndexStorage) + storage._client_factory = MagicMock(remote=True) + + for failure in ( + InvalidArgumentError("invalid filter"), + InvalidDimensionException("wrong embedding dimensions"), + VersionMismatchError("incompatible client and server"), + ValueError("invalid schema"), + ): + with self.subTest(failure=type(failure).__name__): + with self.assertRaises(type(failure)): + storage._call(MagicMock(side_effect=failure)) + + def test_read_only_storage_uses_the_public_collection_api(self): + with TemporaryDirectory() as directory: + client = MagicMock() + client.list_collections.return_value = ["scene"] + factory = MagicMock(remote=False) + factory.create.return_value = client + + with IndexStorage( + replace(self.config, storage_directory=directory), + create=False, + client_factory=factory, + ): + pass + + client.list_collections.assert_called_once_with() + + def test_remote_store_size_is_unknown(self): + storage = object.__new__(IndexStorage) + storage._client_factory = MagicMock(remote=True) + + self.assertIsNone(storage.size_bytes()) + def test_upserts_are_split_into_declared_write_batches(self): collection = FakeCollection() storage = fake_storage(self.config, collection) @@ -142,6 +205,61 @@ def test_query_requests_distances_and_applies_run_and_video_filter(self): self.assertIn({"video_id": "video-1"}, clauses) self.assertIn({"run_id": "run-1"}, clauses) + def test_query_and_records_scope_to_generation_ids(self): + collection = FakeCollection() + storage = fake_storage(self.config, collection) + generation_ids = ("generation-1", "generation-2") + + storage.query( + "scene", + [0.1, 0.2], + top_k=7, + generation_ids=generation_ids, + ) + query_clauses = collection.query_options["where"]["$and"] + self.assertIn( + { + "generation_id": { + "$in": ["generation-1", "generation-2"] + } + }, + query_clauses, + ) + + storage.records( + "scene", + generation_ids=generation_ids, + ) + record_clauses = collection.get_options["where"]["$and"] + self.assertIn( + { + "generation_id": { + "$in": ["generation-1", "generation-2"] + } + }, + record_clauses, + ) + + def test_empty_generation_scope_returns_no_records(self): + collection = FakeCollection() + storage = fake_storage(self.config, collection) + + self.assertEqual( + storage.query( + "scene", + [0.1, 0.2], + top_k=7, + generation_ids=(), + ), + [], + ) + self.assertEqual( + storage.records("scene", generation_ids=()), + [], + ) + self.assertIsNone(collection.query_options) + self.assertIsNone(collection.get_options) + def test_records_apply_capability_filters(self): collection = FakeCollection() storage = fake_storage(self.config, collection) @@ -161,6 +279,19 @@ def test_records_apply_capability_filters(self): self.assertIn({"video_id": "video-1"}, clauses) self.assertIn({"cluster_id": "1"}, clauses) + def test_count_records_uses_ids_without_loading_metadata(self): + collection = FakeCollection() + storage = fake_storage(self.config, collection) + + count = storage.count_records( + "scene", + video_id="video-1", + generation_ids=("generation-1",), + ) + + self.assertEqual(count, 2) + self.assertEqual(collection.get_options["include"], []) + def test_record_cleanup_remains_scoped_to_capability_and_run(self): collection = FakeCollection() storage = fake_storage(self.config, collection) @@ -176,6 +307,19 @@ def test_record_cleanup_remains_scoped_to_capability_and_run(self): self.assertIn({"video_id": "video-1"}, clauses) self.assertIn({"cluster_id": "3"}, clauses) + def test_generation_cleanup_uses_generation_filter(self): + collection = FakeCollection() + storage = fake_storage(self.config, collection) + + storage.delete_generation("generation-1", modalities=("scene",)) + + clauses = collection.deletes[0]["where"]["$and"] + self.assertIn( + {"generation_id": {"$in": ["generation-1"]}}, + clauses, + ) + self.assertIn({"run_id": "run-1"}, clauses) + def test_directory_size_only_counts_files_under_requested_path(self): with TemporaryDirectory() as directory: root = Path(directory) @@ -189,6 +333,70 @@ def test_metadata_filter_keeps_run_identity_without_video_filter(self): where = metadata_filter(self.config) self.assertNotIn({"video_id": None}, where["$and"]) + def test_snapshot_scope_delegates_reads_and_lifecycle(self): + store = MagicMock(spec=IndexStorage) + store.query.return_value = [{"source_id": "source-1"}] + store.records.return_value = [{"source_id": "source-1"}] + store.size_bytes.return_value = 42 + scoped = SnapshotScopedIndexStore( + store, + ("generation-1", "generation-2", "generation-1"), + ) + + with scoped as entered: + self.assertIs(entered, scoped) + self.assertEqual( + scoped.query("scene", [0.1], top_k=2), + [{"source_id": "source-1"}], + ) + self.assertEqual( + scoped.records("scene"), + [{"source_id": "source-1"}], + ) + self.assertEqual(scoped.size_bytes(), 42) + + store.__enter__.assert_called_once_with() + store.__exit__.assert_called_once() + store.query.assert_called_once_with( + "scene", + [0.1], + top_k=2, + video_id=None, + generation_ids=("generation-1", "generation-2"), + filters=None, + ) + store.records.assert_called_once_with( + "scene", + video_id=None, + generation_ids=("generation-1", "generation-2"), + filters=None, + ) + + scoped.close() + store.close.assert_called_once_with() + + def test_snapshot_scope_exposes_no_mutation_api(self): + scoped = SnapshotScopedIndexStore( + MagicMock(spec=IndexStorage), + ("generation-1",), + ) + for method in ( + "clear", + "delete_video", + "delete_records", + "delete_generation", + "upsert", + ): + self.assertFalse(hasattr(scoped, method)) + + def test_explicit_empty_generation_cleanup_selects_nothing(self): + collection = FakeCollection() + storage = fake_storage(self.config, collection) + + storage.delete_generation("generation-1", modalities=()) + + self.assertEqual(collection.deletes, []) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_storage_integration.py b/tests/test_storage_integration.py index c51b45b..6bdc7a3 100644 --- a/tests/test_storage_integration.py +++ b/tests/test_storage_integration.py @@ -1,4 +1,5 @@ import unittest +from pathlib import Path from tempfile import TemporaryDirectory from vidxp.core.contracts import ( @@ -72,6 +73,87 @@ def test_two_videos_can_coexist_query_and_delete_by_video(self): ["video-2"], ) + def test_generation_scope_and_cleanup_use_chroma_in_filter(self): + with TemporaryDirectory() as directory: + config = IndexConfig( + dataset="sample", + split="test", + run_id="run-1", + enabled_modalities=("scene",), + storage_directory=directory, + ) + with IndexStorage(config) as storage: + storage.upsert( + "scene", + [ + StorageRecord( + source_id=f"source-{index}", + embedding=embedding, + metadata={ + "dataset": "sample", + "split": "test", + "run_id": "run-1", + "video_id": f"video-{index}", + "generation_id": generation_id, + }, + ) + for index, generation_id, embedding in ( + (1, "generation-1", [1.0, 0.0]), + (2, "generation-2", [0.0, 1.0]), + ) + ], + batch_size=2, + cancellation=CancellationToken(), + ) + + scoped = storage.query( + "scene", + [1.0, 0.0], + top_k=2, + generation_ids=("generation-1",), + ) + self.assertEqual( + [ + row["metadata"]["generation_id"] + for row in scoped + ], + ["generation-1"], + ) + + storage.delete_generation( + "generation-1", + modalities=("scene",), + ) + remaining = storage.records("scene") + self.assertEqual( + [item["generation_id"] for item in remaining], + ["generation-2"], + ) + + def test_read_only_store_fails_closed_without_database_or_collection(self): + with TemporaryDirectory() as directory: + path = Path(directory) + config = IndexConfig( + dataset="sample", + split="test", + run_id="run-1", + enabled_modalities=("scene",), + storage_directory=path / "missing", + ) + with self.assertRaises(FileNotFoundError): + IndexStorage(config, create=False) + self.assertFalse(config.index_directory.exists()) + + with IndexStorage(config) as storage: + storage.collection("scene") + read_only = IndexStorage(config, create=False) + read_only.client.delete_collection("scene") + with ( + self.assertRaises(FileNotFoundError), + read_only, + ): + read_only.records("scene") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_tusd_hooks.py b/tests/test_tusd_hooks.py new file mode 100644 index 0000000..3915851 --- /dev/null +++ b/tests/test_tusd_hooks.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from pathlib import Path + +from vidxp.application_models import ( + CreateUploadIntentCommand, + Principal, +) +from vidxp.authentication import StaticBearerAuthenticator +from vidxp.authorization import AuthorizationPolicy +from vidxp.infrastructure.sql_catalog import SQLCatalog +from vidxp.infrastructure.tusd_contracts import TusdHookRequest +from vidxp.settings import VidXPSettings +from vidxp.tusd_hooks import TusdHookService +from vidxp.upload_service import RemoteUploadService + + +class _Jobs: + def enqueue_media_import_in_transaction( + self, + upload_id: str, + *, + connection, + job_id: str, + ) -> str: + del upload_id, connection + return job_id + + +def _hooks(tmp_path: Path): + token = "t" * 32 + catalog = SQLCatalog( + f"sqlite:///{(tmp_path / 'server.sqlite3').resolve().as_posix()}", + initialize=True, + ) + settings = VidXPSettings( + repository_root=tmp_path, + upload_public_endpoint="http://localhost:8080/uploads/", + upload_internal_endpoint="http://localhost:8080/uploads/", + upload_cleanup_token="c" * 32, + ) + uploads = RemoteUploadService( + settings=settings, + catalog=catalog, + media=object(), + jobs=_Jobs(), + ) + intent = uploads.create_intent( + CreateUploadIntentCommand( + original_filename="sample.mp4", + byte_size=20, + declared_mime_type="video/mp4", + ), + principal=Principal( + subject="static", + scopes=frozenset({"*"}), + ), + request_key="a" * 64, + ) + return ( + TusdHookService( + uploads=uploads, + authenticator=StaticBearerAuthenticator(token), + authorization=AuthorizationPolicy(), + ), + catalog, + intent, + token, + ) + + +def _pre_create(intent_id: str, token: str) -> TusdHookRequest: + return TusdHookRequest.model_validate( + { + "Type": "pre-create", + "Event": { + "Upload": { + "ID": "", + "Size": 20, + "Offset": 0, + "MetaData": {"intent_id": intent_id}, + }, + "HTTPRequest": { + "Method": "POST", + "URI": "/uploads/", + "Header": {"Authorization": [f"Bearer {token}"]}, + }, + }, + } + ) + + +def test_pre_create_authenticates_and_assigns_stored_id( + tmp_path: Path, +) -> None: + hooks, catalog, intent, token = _hooks(tmp_path) + response = hooks.handle(_pre_create(intent.intent_id, token)) + + assert not response.reject_upload + assert response.change_file_info is not None + stored = catalog.get_upload_intent(intent.intent_id) + assert stored is not None + assert response.change_file_info.upload_id == stored.upload_id + catalog.close() + + +def test_pre_create_rejects_unsupported_tus_modes(tmp_path: Path) -> None: + hooks, catalog, intent, token = _hooks(tmp_path) + request = _pre_create(intent.intent_id, token).model_copy( + update={ + "event": _pre_create(intent.intent_id, token).event.model_copy( + update={ + "upload": _pre_create( + intent.intent_id, + token, + ).event.upload.model_copy( + update={"size_is_deferred": True} + ) + } + ) + } + ) + + response = hooks.handle(request) + + assert response.reject_upload + assert response.http_response is not None + assert response.http_response.status_code == 400 + catalog.close() + + +def test_pre_create_retry_reuses_id_then_rejects_new_materialized_upload( + tmp_path: Path, +) -> None: + hooks, catalog, intent, token = _hooks(tmp_path) + first = hooks.handle(_pre_create(intent.intent_id, token)) + replay = hooks.handle(_pre_create(intent.intent_id, token)) + assert first.change_file_info == replay.change_file_info + assert first.change_file_info is not None + + upload_id = first.change_file_info.upload_id + stored = catalog.get_upload_intent(intent.intent_id) + assert stored is not None + quarantine = tmp_path / "upload-quarantine" + quarantine.mkdir(parents=True, exist_ok=True) + (quarantine / f"{upload_id}.info").write_text("{}", encoding="utf-8") + + duplicate = hooks.handle(_pre_create(intent.intent_id, token)) + assert duplicate.reject_upload + assert duplicate.http_response is not None + assert duplicate.http_response.status_code == 409 + catalog.close() diff --git a/tests/test_upload_service.py b/tests/test_upload_service.py new file mode 100644 index 0000000..fc82b59 --- /dev/null +++ b/tests/test_upload_service.py @@ -0,0 +1,424 @@ +from __future__ import annotations + +from pathlib import Path +from datetime import timedelta +from types import SimpleNamespace +from unittest.mock import patch +from urllib.error import HTTPError +from uuid import uuid4 + +import pytest +from sqlalchemy.exc import IntegrityError + +from vidxp.application_models import ( + ApplicationError, + CreateUploadIntentCommand, + JobState, + Principal, +) +from vidxp.core.media import utc_now +from vidxp.core.uploads import UploadIntentRecord, UploadState +from vidxp.infrastructure.sql_catalog import SQLCatalog +from vidxp.settings import VidXPSettings +from vidxp.upload_service import RemoteUploadService + + +class _Media: + pass + + +class _Jobs: + def __init__(self) -> None: + self.calls: list[tuple[str, str]] = [] + self.states: dict[str, JobState] = {} + + def enqueue_media_import_in_transaction( + self, + upload_id: str, + *, + connection, + job_id: str, + ) -> str: + del connection + self.calls.append((upload_id, job_id)) + return job_id + + def get(self, job_id: str): + return SimpleNamespace(state=self.states[job_id]) + + +def _service( + root: Path, + *, + quota: int = 100, +) -> tuple[RemoteUploadService, SQLCatalog, _Jobs]: + catalog = SQLCatalog( + f"sqlite:///{(root / 'server.sqlite3').resolve().as_posix()}", + initialize=True, + ) + jobs = _Jobs() + settings = VidXPSettings( + repository_root=root, + upload_public_endpoint="http://localhost:8080/uploads/", + upload_internal_endpoint="http://localhost:8080/uploads/", + upload_cleanup_token="x" * 32, + upload_max_bytes=quota, + upload_quota_bytes=quota, + ) + return ( + RemoteUploadService( + settings=settings, + catalog=catalog, + media=_Media(), + jobs=jobs, + ), + catalog, + jobs, + ) + + +def _command(size: int = 60) -> CreateUploadIntentCommand: + return CreateUploadIntentCommand( + original_filename="sample.mp4", + byte_size=size, + declared_mime_type="video/mp4", + ) + + +def test_upload_intent_is_idempotent_and_repository_shared( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + owner = Principal(subject="owner") + created = service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + + assert ( + service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + == created + ) + assert ( + service.get_intent( + created.intent_id, + principal=Principal(subject="other"), + ) + == created + ) + catalog.close() + + +def test_upload_quota_is_reserved_and_released_atomically( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + owner = Principal(subject="owner") + first = service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + + with pytest.raises(ApplicationError) as exceeded: + service.create_intent( + _command(), + principal=owner, + request_key="b" * 64, + ) + assert exceeded.value.detail.code == "upload_quota_exceeded" + + service.accept_creation( + first.intent_id, + principal=owner, + byte_size=60, + ) + service.record_terminated( + catalog.get_upload_intent(first.intent_id).upload_id # type: ignore[union-attr] + ) + second = service.create_intent( + _command(), + principal=owner, + request_key="b" * 64, + ) + assert second.byte_size == 60 + catalog.close() + + +def test_duplicate_finish_enqueues_one_import_job(tmp_path: Path) -> None: + service, catalog, jobs = _service(tmp_path) + owner = Principal(subject="owner") + intent = service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + accepted = service.accept_creation( + intent.intent_id, + principal=owner, + byte_size=60, + ) + assert accepted.upload_id is not None + + first = service.complete_upload( + intent_id=intent.intent_id, + upload_id=accepted.upload_id, + byte_size=60, + offset=60, + ) + second = service.complete_upload( + intent_id=intent.intent_id, + upload_id=accepted.upload_id, + byte_size=60, + offset=60, + ) + + assert first == second == accepted.upload_id + assert jobs.calls == [(accepted.upload_id, accepted.upload_id)] + catalog.close() + + +def test_creation_hook_retry_replays_only_before_tusd_materializes_upload( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + owner = Principal(subject="owner") + intent = service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + + first = service.accept_creation( + intent.intent_id, + principal=owner, + byte_size=60, + ) + replay = service.accept_creation( + intent.intent_id, + principal=owner, + byte_size=60, + ) + + assert first.upload_id == replay.upload_id + assert first.upload_id is not None + service.settings.quarantine_root.mkdir(parents=True, exist_ok=True) + (service.settings.quarantine_root / f"{first.upload_id}.info").write_text( + "{}", + encoding="utf-8", + ) + with pytest.raises(ApplicationError) as duplicate: + service.accept_creation( + intent.intent_id, + principal=owner, + byte_size=60, + ) + assert duplicate.value.detail.code == "upload_already_created" + catalog.close() + + +def test_termination_reserves_state_before_finish_can_enqueue( + tmp_path: Path, +) -> None: + service, catalog, jobs = _service(tmp_path) + owner = Principal(subject="owner") + intent = service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + accepted = service.accept_creation( + intent.intent_id, + principal=owner, + byte_size=60, + ) + assert accepted.upload_id is not None + + service.authorize_termination( + accepted.upload_id, + cleanup_token=None, + ) + + with pytest.raises(ApplicationError) as rejected: + service.complete_upload( + intent_id=intent.intent_id, + upload_id=accepted.upload_id, + byte_size=60, + offset=60, + ) + assert rejected.value.detail.code == "upload_completion_invalid" + assert jobs.calls == [] + stored = catalog.get_upload_intent(intent.intent_id) + assert stored is not None and stored.state == UploadState.expired + catalog.close() + + +def test_expired_creation_persists_state_and_releases_quota( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + now = utc_now() + record = UploadIntentRecord( + intent_id=uuid4().hex, + request_key="a" * 64, + original_filename="sample.mp4", + byte_size=60, + declared_mime_type="video/mp4", + state=UploadState.pending, + created_at=now - timedelta(minutes=10), + expires_at=now - timedelta(minutes=5), + ) + catalog.create_upload_intent(record, quota_limit=100) + + with pytest.raises(ApplicationError) as expired: + service.accept_creation( + record.intent_id, + principal=Principal(subject="owner"), + byte_size=60, + ) + + assert expired.value.detail.code == "upload_intent_expired" + stored = catalog.get_upload_intent(record.intent_id) + assert stored is not None and stored.state == UploadState.expired + service.create_intent( + _command(), + principal=Principal(subject="owner"), + request_key="b" * 64, + ) + catalog.close() + + +def test_active_resumable_upload_is_not_expired(tmp_path: Path) -> None: + service, catalog, _ = _service(tmp_path) + now = utc_now() + upload_id = "2" * 32 + record = UploadIntentRecord( + intent_id=uuid4().hex, + request_key="a" * 64, + original_filename="sample.mp4", + byte_size=60, + declared_mime_type="video/mp4", + state=UploadState.accepted, + created_at=now - timedelta(days=2), + expires_at=now - timedelta(days=1), + upload_id=upload_id, + ) + catalog.create_upload_intent(record, quota_limit=100) + service.settings.quarantine_root.mkdir(parents=True, exist_ok=True) + (service.settings.quarantine_root / f"{upload_id}.info").write_text( + '{"MetaData":{"intent_id":"' + + record.intent_id + + '"},"Size":60,"Offset":10}', + encoding="utf-8", + ) + + result = service.reconcile() + + assert result["expired"] == 0 + stored = catalog.get_upload_intent(record.intent_id) + assert stored is not None and stored.state == UploadState.accepted + catalog.close() + + +def test_concurrent_idempotency_winner_must_match_request( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + original_create = catalog.create_upload_intent + + def lose_race(record, *, quota_limit): + winner = record.model_copy( + update={ + "intent_id": uuid4().hex, + "original_filename": "other.mp4", + } + ) + original_create(winner, quota_limit=quota_limit) + raise IntegrityError("insert upload", {}, RuntimeError("duplicate")) + + with patch.object(catalog, "create_upload_intent", side_effect=lose_race): + with pytest.raises(ApplicationError) as conflict: + service.create_intent( + _command(), + principal=Principal(subject="owner"), + request_key="a" * 64, + ) + + assert conflict.value.detail.code == "idempotency_key_reused" + catalog.close() + + +def test_terminal_import_job_releases_processing_upload( + tmp_path: Path, +) -> None: + service, catalog, jobs = _service(tmp_path) + owner = Principal(subject="owner") + intent = service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + accepted = service.accept_creation( + intent.intent_id, + principal=owner, + byte_size=60, + ) + assert accepted.upload_id is not None + job_id = service.complete_upload( + intent_id=intent.intent_id, + upload_id=accepted.upload_id, + byte_size=60, + offset=60, + ) + jobs.states[job_id] = JobState.cancelled + + result = service.reconcile() + + assert result["failed"] == 1 + stored = catalog.get_upload_intent(intent.intent_id) + assert stored is not None and stored.state == UploadState.failed + catalog.close() + + +def test_cleanup_reconciles_tusd_not_found_and_clears_capability_url( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + owner = Principal(subject="owner") + intent = service.create_intent( + _command(), + principal=owner, + request_key="a" * 64, + ) + accepted = service.accept_creation( + intent.intent_id, + principal=owner, + byte_size=60, + ) + assert accepted.upload_id is not None + service.authorize_termination( + accepted.upload_id, + cleanup_token=None, + ) + missing = HTTPError( + url="http://localhost/uploads/missing", + code=404, + msg="not found", + hdrs=None, + fp=None, + ) + + with patch("vidxp.upload_service.urlopen", side_effect=missing): + result = service.reconcile() + + assert result["cleaned"] == 1 + stored = catalog.get_upload_intent(intent.intent_id) + assert stored is not None + assert stored.state == UploadState.expired + assert stored.upload_id is None + catalog.close() diff --git a/tests/test_video.py b/tests/test_video.py index ac35371..282b4b4 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -7,6 +7,7 @@ from vidxp.core.contracts import CancellationToken from vidxp.core.video import ( + FrameSampling, FrameStreamStats, iter_frame_batches, render_actor_video, @@ -14,15 +15,16 @@ class FakeCapture: - def __init__(self, frames): + def __init__(self, frames, *, fps=10.0): self.frames = list(frames) + self.fps = fps self.position = 0 self.read_calls = 0 self.grab_calls = 0 self.released = False def get(self, _): - return 10.0 + return self.fps def isOpened(self): return True @@ -75,6 +77,58 @@ def test_stride_advances_skipped_frames_without_materializing_them(self): self.assertEqual(stats.frames_materialized, 2) self.assertTrue(capture.released) + def test_multiple_cadences_materialize_their_union_once(self): + capture = FakeCapture([f"f{index}" for index in range(8)]) + fake_cv2 = types.SimpleNamespace( + CAP_PROP_FPS=5, + VideoCapture=lambda _: capture, + ) + + with patch.dict(sys.modules, {"cv2": fake_cv2}): + batches = list( + iter_frame_batches( + "unused.mp4", + frame_strides=(3, 4), + batch_size=8, + cancellation=CancellationToken(), + ) + ) + + self.assertEqual( + [sample.frame_index for sample in batches[0]], + [0, 3, 4, 6], + ) + self.assertEqual(capture.read_calls, 5) + self.assertEqual(capture.grab_calls, 4) + + def test_time_and_frame_cadences_share_one_materialized_union(self): + capture = FakeCapture( + [f"f{index}" for index in range(6)], + fps=1.5, + ) + fake_cv2 = types.SimpleNamespace( + CAP_PROP_FPS=5, + VideoCapture=lambda _: capture, + ) + + with patch.dict(sys.modules, {"cv2": fake_cv2}): + batches = list( + iter_frame_batches( + "unused.mp4", + samplings=( + FrameSampling(source_fps=1.5, target_fps=1.0), + FrameSampling(frame_stride=2), + ), + batch_size=8, + cancellation=CancellationToken(), + ) + ) + + self.assertEqual( + [sample.frame_index for sample in batches[0]], + [0, 2, 3, 4, 5], + ) + def test_actor_renderer_creates_output_directory_and_falls_back_codec(self): with TemporaryDirectory() as directory: input_path = Path(directory) / "input.mp4" @@ -105,6 +159,7 @@ def writer(path, codec, *_): CAP_PROP_FPS=1, CAP_PROP_FRAME_WIDTH=2, CAP_PROP_FRAME_HEIGHT=3, + CAP_PROP_FRAME_COUNT=5, FONT_HERSHEY_SIMPLEX=4, VideoCapture=lambda _: capture, VideoWriter=writer, From 5ae9fc7bbb54dfcbca4a7597604d8466dc3b978c Mon Sep 17 00:00:00 2001 From: Talha Date: Fri, 31 Jul 2026 00:21:07 +0500 Subject: [PATCH 2/8] feat(interfaces): expose CLI, browser, API, and MCP surfaces --- MANIFEST.in | 1 + pyproject.toml | 60 +- setup.py | 17 + src/vidxp/__main__.py | 2 +- src/vidxp/api.py | 162 + src/vidxp/api_cli.py | 41 + src/vidxp/api_errors.py | 133 + src/vidxp/api_middleware.py | 319 ++ src/vidxp/api_models.py | 29 + src/vidxp/api_routes/__init__.py | 23 + src/vidxp/api_routes/artifacts.py | 75 + src/vidxp/api_routes/dependencies.py | 177 + src/vidxp/api_routes/index.py | 26 + src/vidxp/api_routes/jobs.py | 313 ++ src/vidxp/api_routes/media.py | 240 + src/vidxp/api_routes/platform.py | 54 + src/vidxp/cli.py | 113 +- .../actor/cli.py => cli_commands/actors.py} | 113 +- src/vidxp/cli_commands/artifacts.py | 215 + src/vidxp/cli_commands/index.py | 196 +- src/vidxp/cli_commands/jobs.py | 105 + src/vidxp/cli_commands/mcp.py | 64 + src/vidxp/cli_commands/media.py | 123 + src/vidxp/cli_commands/query.py | 98 + src/vidxp/cli_commands/runtime.py | 515 +- src/vidxp/cli_commands/search.py | 116 +- src/vidxp/cli_support.py | 167 +- src/vidxp/database_cli.py | 39 + src/vidxp/entrypoint.py | 83 + src/vidxp/frontend.py | 1074 +++- src/vidxp/health_cli.py | 49 + src/vidxp/hook_app.py | 85 + src/vidxp/hook_cli.py | 20 + src/vidxp/mcp.py | 823 +++ src/vidxp/mcp_cli.py | 202 + tests/test_api.py | 1092 ++++ tests/test_api_cli.py | 20 + tests/test_cli.py | 1238 +++-- tests/test_database_cli.py | 37 + tests/test_frontend.py | 896 +++- tests/test_frontend_app.py | 358 ++ tests/test_mcp.py | 733 +++ tests/test_public_path_contracts.py | 197 + uv.lock | 4668 +++++++++++++++++ 44 files changed, 14152 insertions(+), 959 deletions(-) create mode 100644 setup.py create mode 100644 src/vidxp/api.py create mode 100644 src/vidxp/api_cli.py create mode 100644 src/vidxp/api_errors.py create mode 100644 src/vidxp/api_middleware.py create mode 100644 src/vidxp/api_models.py create mode 100644 src/vidxp/api_routes/__init__.py create mode 100644 src/vidxp/api_routes/artifacts.py create mode 100644 src/vidxp/api_routes/dependencies.py create mode 100644 src/vidxp/api_routes/index.py create mode 100644 src/vidxp/api_routes/jobs.py create mode 100644 src/vidxp/api_routes/media.py create mode 100644 src/vidxp/api_routes/platform.py rename src/vidxp/{capabilities/actor/cli.py => cli_commands/actors.py} (62%) create mode 100644 src/vidxp/cli_commands/artifacts.py create mode 100644 src/vidxp/cli_commands/jobs.py create mode 100644 src/vidxp/cli_commands/mcp.py create mode 100644 src/vidxp/cli_commands/media.py create mode 100644 src/vidxp/cli_commands/query.py create mode 100644 src/vidxp/database_cli.py create mode 100644 src/vidxp/entrypoint.py create mode 100644 src/vidxp/health_cli.py create mode 100644 src/vidxp/hook_app.py create mode 100644 src/vidxp/hook_cli.py create mode 100644 src/vidxp/mcp.py create mode 100644 src/vidxp/mcp_cli.py create mode 100644 tests/test_api.py create mode 100644 tests/test_api_cli.py create mode 100644 tests/test_database_cli.py create mode 100644 tests/test_frontend_app.py create mode 100644 tests/test_mcp.py create mode 100644 tests/test_public_path_contracts.py create mode 100644 uv.lock diff --git a/MANIFEST.in b/MANIFEST.in index 993b28c..383bfce 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include README.md include LICENSE +include docs/images/logo.png global-exclude *.whl global-exclude *.pyc global-exclude __pycache__ diff --git a/pyproject.toml b/pyproject.toml index cfc7678..af3a404 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ authors = [ ] description = "VidXP - Video indexing and search by dialogue, scene, and actor" readme = "README.md" -requires-python = ">=3.10,<3.14" +requires-python = ">=3.11,<3.15" license = "MIT" license-files = ["LICENSE"] keywords = ["video", "search", "indexing", "cli", "streamlit"] @@ -26,26 +26,36 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Multimedia :: Video", "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["optional-dependencies"] dependencies = [ - "filelock>=3.13", - "packaging>=24", - "pydantic>=2.8,<3", - "rich", + "dbos>=2.28,<3", + "filelock>=3.32,<4", + "packaging>=26.2,<27", + "platformdirs>=4.11,<5", + "pydantic>=2.13.4,<3", + "pydantic-settings>=2.14.2,<3", + "rich>=15,<16", + "sqlalchemy>=2.0.51,<2.1", "typer>=0.27,<1", ] [project.scripts] -vidxp = "vidxp.cli:main" +vidxp = "vidxp.entrypoint:main" +vidxp-api = "vidxp.api_cli:main" +vidxp-worker = "vidxp.workflow_worker:main" +vidxp-database = "vidxp.database_cli:main" +vidxp-hooks = "vidxp.hook_cli:main" +vidxp-mcp = "vidxp.mcp_cli:main" [project.urls] +Homepage = "https://github.com/grayhatdevelopers/vidxp" Repository = "https://github.com/grayhatdevelopers/vidxp" Issues = "https://github.com/grayhatdevelopers/vidxp/issues" @@ -62,6 +72,8 @@ vidxp = [ "benchmarks/requirements.txt", "capabilities/*/requirements.txt", "requirements/*.txt", + "migrations/*.py", + "migrations/versions/*.py", ] [tool.setuptools.dynamic.optional-dependencies] @@ -84,15 +96,47 @@ all = { file = [ "src/vidxp/capabilities/scene/requirements.txt", "src/vidxp/capabilities/actor/requirements.txt", ] } +local-worker = { file = [ + "src/vidxp/requirements/storage.txt", + "src/vidxp/requirements/slm.txt", + "src/vidxp/capabilities/dialogue/requirements.txt", + "src/vidxp/capabilities/scene/requirements.txt", + "src/vidxp/capabilities/actor/requirements.txt", +] } +mcp = { file = ["src/vidxp/requirements/mcp.txt"] } +slm = { file = ["src/vidxp/requirements/slm.txt"] } +server = { file = [ + "src/vidxp/requirements/server.txt", + "src/vidxp/requirements/mcp.txt", +] } +server-worker = { file = [ + "src/vidxp/requirements/server.txt", + "src/vidxp/requirements/server-storage.txt", + "src/vidxp/requirements/slm.txt", + "src/vidxp/capabilities/dialogue/requirements.txt", + "src/vidxp/capabilities/scene/requirements.txt", + "src/vidxp/capabilities/actor/requirements.txt", +] } +test = { file = ["src/vidxp/requirements/test.txt"] } frontend = { file = ["src/vidxp/requirements/frontend.txt"] } benchmarks = { file = ["src/vidxp/benchmarks/requirements.txt"] } [tool.ruff] -target-version = "py310" +target-version = "py311" [tool.ruff.lint] select = ["E4", "E7", "E9", "F"] +[tool.uv.sources] +torch = [ + { index = "pytorch-cpu", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true + [tool.semantic_release] version_toml = ["pyproject.toml:project.version"] tag_format = "v{version}" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..31da439 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from pathlib import Path +from shutil import copyfile + +from setuptools import setup +from setuptools.command.build_py import build_py + + +class BuildPy(build_py): + def run(self): + super().run() + source = Path(__file__).parent / "docs" / "images" / "logo.png" + target = Path(self.build_lib) / "vidxp" / "assets" / "icon.png" + target.parent.mkdir(parents=True, exist_ok=True) + copyfile(source, target) + + +setup(cmdclass={"build_py": BuildPy}) diff --git a/src/vidxp/__main__.py b/src/vidxp/__main__.py index 2f05ddc..d952b81 100644 --- a/src/vidxp/__main__.py +++ b/src/vidxp/__main__.py @@ -1,4 +1,4 @@ -from .cli import main +from .entrypoint import main if __name__ == "__main__": diff --git a/src/vidxp/api.py b/src/vidxp/api.py new file mode 100644 index 0000000..8431d5c --- /dev/null +++ b/src/vidxp/api.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from contextlib import asynccontextmanager +from typing import Annotated, AsyncIterator + +from asgi_correlation_id import CorrelationIdMiddleware +from fastapi import Depends, FastAPI, Request, Response, Security +from fastapi.security import HTTPBearer +from vidxp import __version__ +from vidxp.api_errors import install_exception_handlers +from vidxp.api_middleware import ( + ApiCORSMiddleware, + BearerAuthenticationMiddleware, + RequestBodyLimitMiddleware, + RequestBodyTooLarge, + TypedTrustedHostMiddleware, + request_too_large_response, +) +from vidxp.api_models import HealthResponse, ReadinessResponse +from vidxp.api_routes import create_api_router +from vidxp.api_routes.dependencies import context +from vidxp.composition import HttpApplicationContext, create_http_application +from vidxp.mcp import MCPTransportSecurityBoundary, create_remote_mcp +from vidxp.settings import VidXPSettings + + +_BEARER_SECURITY = HTTPBearer( + auto_error=False, + scheme_name="BearerAuth", + description=( + "Bearer access token. Authentication is enforced once by the " + "server middleware." + ), +) + + +def create_app( + settings: VidXPSettings | None = None, + *, + context: HttpApplicationContext | None = None, +) -> FastAPI: + active_context = context or create_http_application(settings) + active_settings = active_context.settings + active_settings.validate_http_server() + owns_context = context is None + try: + remote_mcp = create_remote_mcp(active_context) + except Exception: + if owns_context: + active_context.close() + raise + mcp_paths = ("/mcp", "/mcp/") + delegated_auth_paths = ( + ( + *mcp_paths, + "/.well-known/oauth-protected-resource/mcp", + "/.well-known/oauth-protected-resource/mcp/", + ) + if remote_mcp.owns_authentication + else () + ) + + @asynccontextmanager + async def lifespan(_app: FastAPI) -> AsyncIterator[None]: + try: + active_context.jobs.start() + async with remote_mcp.server.session_manager.run(): + yield + finally: + if owns_context: + active_context.close() + + publish_docs = active_settings.http_auth_mode.value == "none" + app = FastAPI( + title="VidXP API", + version=__version__, + lifespan=lifespan, + docs_url="/docs" if publish_docs else None, + redoc_url="/redoc" if publish_docs else None, + openapi_url="/openapi.json", + ) + app.state.vidxp = active_context + install_exception_handlers(app) + + @app.get( + "/health", + response_model=HealthResponse, + operation_id="health", + summary="Check process liveness", + ) + def health() -> HealthResponse: + return HealthResponse() + + @app.get( + "/ready", + response_model=ReadinessResponse, + responses={503: {"model": ReadinessResponse}}, + operation_id="ready", + summary="Check aggregate readiness", + ) + def ready( + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + ) -> ReadinessResponse: + is_ready = service.readiness.ready() + if not is_ready: + response.status_code = 503 + return ReadinessResponse( + ready=is_ready, + status="ready" if is_ready else "not_ready", + ) + + api_dependencies = ( + [Security(_BEARER_SECURITY)] + if active_settings.http_auth_mode.value != "none" + else [] + ) + app.include_router( + create_api_router(), + dependencies=api_dependencies, + ) + app.mount("/", remote_mcp.app) + app.add_exception_handler( + RequestBodyTooLarge, + _request_body_too_large_response, + ) + app.add_middleware( + RequestBodyLimitMiddleware, + json_limit=active_settings.http_max_json_body_bytes, + upload_limit=active_settings.http_max_small_upload_bytes, + delegated_paths=mcp_paths, + ) + app.add_middleware( + BearerAuthenticationMiddleware, + authenticator=active_context.authenticator, + delegated_paths=delegated_auth_paths, + ) + if active_settings.http_allowed_origins: + app.add_middleware( + ApiCORSMiddleware, + allow_origins=active_settings.http_allowed_origins, + ) + app.add_middleware( + TypedTrustedHostMiddleware, + allowed_hosts=active_settings.http_trusted_hosts, + ) + app.add_middleware( + CorrelationIdMiddleware, + header_name="X-Request-ID", + ) + app.add_middleware( + MCPTransportSecurityBoundary, + settings=remote_mcp.transport_security, + ) + return app + + +async def _request_body_too_large_response( + _request: Request, + _exc: RequestBodyTooLarge, +) -> Response: + return request_too_large_response() diff --git a/src/vidxp/api_cli.py b/src/vidxp/api_cli.py new file mode 100644 index 0000000..4b49c32 --- /dev/null +++ b/src/vidxp/api_cli.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import argparse +from collections.abc import Sequence +from pathlib import Path + + +def _arguments(values: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Run the VidXP HTTP API and remote MCP server. " + "Configure the service with VIDXP_* environment variables." + ) + ) + parser.add_argument( + "--data-dir", + type=Path, + help="Store VidXP models and the default repository here.", + ) + return parser.parse_args(values) + + +def main(arguments: Sequence[str] | None = None) -> None: + options = _arguments(arguments) + + import uvicorn + + from vidxp.api import create_app + from vidxp.settings import VidXPSettings + + settings = ( + VidXPSettings(data_dir=options.data_dir) + if options.data_dir is not None + else VidXPSettings() + ) + settings.validate_http_server() + uvicorn.run( + create_app(settings), + host=settings.http_bind_host, + port=settings.http_port, + ) diff --git a/src/vidxp/api_errors.py b/src/vidxp/api_errors.py new file mode 100644 index 0000000..887ff90 --- /dev/null +++ b/src/vidxp/api_errors.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import logging + +from asgi_correlation_id import correlation_id +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException + +from vidxp.api_models import ErrorEnvelope +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + ErrorDetail, + InvalidRequestError, +) + + +LOGGER = logging.getLogger("vidxp.api") + +_STATUS_BY_CATEGORY = { + ErrorCategory.validation: 422, + ErrorCategory.authentication: 401, + ErrorCategory.authorization: 403, + ErrorCategory.not_found: 404, + ErrorCategory.conflict: 409, + ErrorCategory.unavailable: 503, + ErrorCategory.resource_limit: 429, + ErrorCategory.cancelled: 409, + ErrorCategory.internal: 500, +} + + +def public_error_response( + detail: ErrorDetail, + *, + status_code: int | None = None, + headers: dict[str, str] | None = None, +) -> JSONResponse: + request_id = correlation_id.get() + if detail.correlation_id is None and request_id is not None: + detail = detail.model_copy(update={"correlation_id": request_id}) + active_headers = dict(headers or {}) + if detail.category == ErrorCategory.authentication: + active_headers.setdefault("WWW-Authenticate", "Bearer") + return JSONResponse( + status_code=status_code or _STATUS_BY_CATEGORY[detail.category], + content=ErrorEnvelope(error=detail).model_dump(mode="json"), + headers=active_headers, + ) + + +def _validation_error(request_error: RequestValidationError) -> ErrorDetail: + errors = [ + { + "type": item["type"], + "location": [str(part) for part in item["loc"]], + "message": item["msg"], + } + for item in request_error.errors() + ] + return InvalidRequestError(errors=errors).detail + + +def install_exception_handlers(app: FastAPI) -> None: + @app.exception_handler(ApplicationError) + async def application_error_handler( + _request: Request, + exc: ApplicationError, + ) -> JSONResponse: + status = ( + 413 + if exc.code in {"media_too_large", "request_body_too_large"} + else None + ) + return public_error_response(exc.detail, status_code=status) + + @app.exception_handler(RequestValidationError) + async def request_validation_handler( + _request: Request, + exc: RequestValidationError, + ) -> JSONResponse: + return public_error_response(_validation_error(exc), status_code=422) + + @app.exception_handler(HTTPException) + async def http_error_handler( + _request: Request, + exc: HTTPException, + ) -> JSONResponse: + if exc.status_code == 404: + detail = ErrorDetail( + code="http_not_found", + category=ErrorCategory.not_found, + message="The requested HTTP resource was not found.", + ) + elif exc.status_code == 405: + detail = ErrorDetail( + code="http_method_not_allowed", + category=ErrorCategory.validation, + message="The HTTP method is not allowed for this resource.", + ) + else: + detail = ErrorDetail( + code="http_request_rejected", + category=ErrorCategory.validation, + message="The HTTP request was rejected.", + ) + return public_error_response( + detail, + status_code=exc.status_code, + headers=exc.headers, + ) + + @app.exception_handler(Exception) + async def unexpected_error_handler( + request: Request, + exc: Exception, + ) -> JSONResponse: + LOGGER.exception( + "Unhandled API error for %s %s", + request.method, + request.url.path, + exc_info=exc, + ) + return public_error_response( + ErrorDetail( + code="internal_error", + category=ErrorCategory.internal, + message="The request could not be completed.", + ), + status_code=500, + ) diff --git a/src/vidxp/api_middleware.py b/src/vidxp/api_middleware.py new file mode 100644 index 0000000..c13ae4d --- /dev/null +++ b/src/vidxp/api_middleware.py @@ -0,0 +1,319 @@ +from __future__ import annotations + +from fastapi.security.utils import get_authorization_scheme_param +from starlette.concurrency import run_in_threadpool +from starlette.datastructures import Headers +from starlette.exceptions import HTTPException +from starlette.middleware.cors import CORSMiddleware, SAFELISTED_HEADERS +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +from vidxp.api_errors import public_error_response +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + ErrorDetail, +) +from vidxp.authentication import Authenticator + + +PUBLIC_HTTP_PATHS = frozenset({"/health", "/ready"}) +UPLOAD_PATH = "/api/v1/media" + + +class RequestBodyTooLarge(HTTPException, OSError): + """Abort body parsing while preserving Starlette's file cleanup path.""" + + def __init__(self) -> None: + super().__init__(status_code=413) + + +class ApiCORSMiddleware: + """Apply Starlette CORS policy only to the REST namespace.""" + + def __init__( + self, + app: ASGIApp, + *, + allow_origins: tuple[str, ...], + ) -> None: + self.app = app + self.allow_origins = frozenset(allow_origins) + self.allow_methods = frozenset({"DELETE", "GET", "HEAD", "POST"}) + self.allow_headers = frozenset( + header.lower() + for header in ( + *SAFELISTED_HEADERS, + "Authorization", + "Idempotency-Key", + "Range", + "X-Request-ID", + ) + ) + self.cors = CORSMiddleware( + app, + allow_origins=list(allow_origins), + allow_credentials=False, + allow_methods=sorted(self.allow_methods), + allow_headers=[ + "Authorization", + "Content-Type", + "Idempotency-Key", + "Range", + "X-Request-ID", + ], + expose_headers=[ + "Accept-Ranges", + "Content-Disposition", + "Content-Length", + "Content-Range", + "ETag", + "Location", + "X-Request-ID", + ], + max_age=600, + ) + + async def __call__( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + if ( + scope["type"] == "http" + and str(scope.get("path", "")).startswith("/api/") + ): + headers = Headers(scope=scope) + origin = headers.get("origin") + requested_method = headers.get("access-control-request-method") + if origin is not None and requested_method is not None: + requested_headers = { + value.strip().lower() + for value in headers.get( + "access-control-request-headers", + "", + ).split(",") + if value.strip() + } + if ( + "*" not in self.allow_origins + and origin not in self.allow_origins + ): + await public_error_response( + ErrorDetail( + code="cors_origin_forbidden", + category=ErrorCategory.authorization, + message="The browser origin is not allowed.", + ), + status_code=403, + )(scope, receive, send) + return + if ( + requested_method not in self.allow_methods + or not requested_headers.issubset(self.allow_headers) + ): + await public_error_response( + ErrorDetail( + code="cors_preflight_invalid", + category=ErrorCategory.validation, + message="The CORS preflight request is invalid.", + ), + status_code=400, + )(scope, receive, send) + return + await self.cors(scope, receive, send) + return + await self.app(scope, receive, send) + + +class TypedTrustedHostMiddleware: + def __init__( + self, + app: ASGIApp, + *, + allowed_hosts: tuple[str, ...], + ) -> None: + self.app = app + self.allowed_hosts = allowed_hosts + self.allow_any = "*" in allowed_hosts + + async def __call__( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + if self.allow_any or scope["type"] != "http": + await self.app(scope, receive, send) + return + host = _host_name(Headers(scope=scope).get("host", "")) + if any( + host == pattern + or ( + pattern.startswith("*.") + and host.endswith(pattern[1:]) + ) + for pattern in self.allowed_hosts + ): + await self.app(scope, receive, send) + return + await public_error_response( + ErrorDetail( + code="host_not_allowed", + category=ErrorCategory.validation, + message="The HTTP Host header is not allowed.", + ), + status_code=400, + )(scope, receive, send) + + +def _host_name(header: str) -> str: + value = header.lower() + if value.startswith("["): + closing = value.find("]") + if closing < 0: + return "" + remainder = value[closing + 1 :] + if remainder and ( + not remainder.startswith(":") + or not remainder[1:].isdigit() + ): + return "" + return value[1:closing] + if value.count(":") == 1: + candidate, port = value.rsplit(":", 1) + if port.isdigit(): + return candidate + return value + + +class BearerAuthenticationMiddleware: + def __init__( + self, + app: ASGIApp, + *, + authenticator: Authenticator, + delegated_paths: tuple[str, ...] = (), + ) -> None: + self.app = app + self.authenticator = authenticator + self.delegated_paths = frozenset(delegated_paths) + + async def __call__( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + if ( + scope["type"] != "http" + or str(scope.get("path", "")) in PUBLIC_HTTP_PATHS + or str(scope.get("path", "")) in self.delegated_paths + ): + await self.app(scope, receive, send) + return + authorization = Headers(scope=scope).get("authorization") + scheme, credentials = get_authorization_scheme_param(authorization) + token = credentials if scheme.lower() == "bearer" else None + try: + principal = await run_in_threadpool( + self.authenticator.authenticate, + token, + ) + except ApplicationError as exc: + await public_error_response(exc.detail)( + scope, + receive, + send, + ) + return + scope["vidxp.principal"] = principal + await self.app(scope, receive, send) + + +class RequestBodyLimitMiddleware: + def __init__( + self, + app: ASGIApp, + *, + json_limit: int, + upload_limit: int, + delegated_paths: tuple[str, ...] = (), + ) -> None: + self.app = app + self.json_limit = json_limit + self.upload_limit = upload_limit + self.delegated_paths = frozenset(delegated_paths) + + async def __call__( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + if str(scope.get("path", "")) in self.delegated_paths: + await self.app(scope, receive, send) + return + limit = ( + self.upload_limit + 1024 * 1024 + if scope.get("path") == UPLOAD_PATH + else self.json_limit + ) + headers = Headers(scope=scope) + content_length = headers.get("content-length") + if content_length is not None: + try: + declared = int(content_length) + except ValueError: + await public_error_response( + ErrorDetail( + code="content_length_invalid", + category=ErrorCategory.validation, + message="The Content-Length header is invalid.", + ), + status_code=400, + )(scope, receive, send) + return + if declared < 0: + await public_error_response( + ErrorDetail( + code="content_length_invalid", + category=ErrorCategory.validation, + message="The Content-Length header is invalid.", + ), + status_code=400, + )(scope, receive, send) + return + if declared > limit: + await request_too_large_response()(scope, receive, send) + return + + received = 0 + + async def limited_receive() -> Message: + nonlocal received + message = await receive() + if message["type"] == "http.request": + received += len(message.get("body", b"")) + if received > limit: + raise RequestBodyTooLarge + return message + + try: + await self.app(scope, limited_receive, send) + except RequestBodyTooLarge: + await request_too_large_response()(scope, receive, send) + + +def request_too_large_response(): + return public_error_response( + ErrorDetail( + code="request_body_too_large", + category=ErrorCategory.resource_limit, + message="The HTTP request body exceeds the configured limit.", + ), + status_code=413, + ) diff --git a/src/vidxp/api_models.py b/src/vidxp/api_models.py new file mode 100644 index 0000000..6f7f269 --- /dev/null +++ b/src/vidxp/api_models.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Literal + +from vidxp.application_models import ( + ApplicationModel, + ErrorDetail, + UploadIntent, +) + + +class ErrorEnvelope(ApplicationModel): + error: ErrorDetail + + +class HealthResponse(ApplicationModel): + status: Literal["ok"] = "ok" + + +class ReadinessResponse(ApplicationModel): + ready: bool + status: Literal["ready", "not_ready"] + + +class UploadIntentResponse(ApplicationModel): + intent: UploadIntent + creation_url: str + upload_metadata: str + resume_url: str | None = None diff --git a/src/vidxp/api_routes/__init__.py b/src/vidxp/api_routes/__init__.py new file mode 100644 index 0000000..caddbab --- /dev/null +++ b/src/vidxp/api_routes/__init__.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter + +from vidxp.api_models import ErrorEnvelope +from vidxp.api_routes import artifacts, index, jobs, media, platform + + +ERROR_RESPONSES = { + status: {"model": ErrorEnvelope} + for status in (400, 401, 403, 404, 409, 413, 422, 429, 500, 503) +} + + +def create_api_router() -> APIRouter: + router = APIRouter( + prefix="/api/v1", + responses=ERROR_RESPONSES, + ) + router.include_router(platform.router) + router.include_router(media.router) + router.include_router(index.router) + router.include_router(jobs.router) + router.include_router(artifacts.router) + return router diff --git a/src/vidxp/api_routes/artifacts.py b/src/vidxp/api_routes/artifacts.py new file mode 100644 index 0000000..1b2c3d7 --- /dev/null +++ b/src/vidxp/api_routes/artifacts.py @@ -0,0 +1,75 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Request, Response + +from vidxp.api_routes.dependencies import ( + context, + file_response, + read_principal, +) +from vidxp.application_models import Artifact +from vidxp.composition import HttpApplicationContext +from vidxp.core.identifiers import ArtifactId + + +router = APIRouter( + prefix="/artifacts", + tags=["artifacts"], + dependencies=[Depends(read_principal)], +) + + +@router.get( + "/{artifact_id}", + response_model=Artifact, + operation_id="getArtifact", + summary="Get artifact metadata", +) +def get_artifact( + artifact_id: ArtifactId, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> Artifact: + return service.application.get_artifact(artifact_id) + + +def _content( + artifact_id: ArtifactId, + request: Request, + service: HttpApplicationContext, +) -> Response: + return file_response( + request, + service.application.open_artifact_content(artifact_id), + disposition="attachment", + ) + + +@router.get( + "/{artifact_id}/content", + response_model=None, + operation_id="getArtifactContent", + summary="Download a generated clip or artifact", + description=( + "Stream the generated artifact with attachment, byte-range, and ETag " + "support. Use the artifact_id returned by a completed snippet or " + "actor-overlay job." + ), +) +def get_artifact_content( + artifact_id: ArtifactId, + request: Request, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> Response: + return _content(artifact_id, request, service) + + +@router.head( + "/{artifact_id}/content", + include_in_schema=False, +) +def head_artifact_content( + artifact_id: ArtifactId, + request: Request, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> Response: + return _content(artifact_id, request, service) diff --git a/src/vidxp/api_routes/dependencies.py b/src/vidxp/api_routes/dependencies.py new file mode 100644 index 0000000..15afef6 --- /dev/null +++ b/src/vidxp/api_routes/dependencies.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import os +import tempfile +from pathlib import Path +from typing import Annotated + +from fastapi import Header, Request, Response, UploadFile +from fastapi.responses import FileResponse + +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + Job, + Principal, +) +from vidxp.composition import HttpApplicationContext +from vidxp.authorization import AuthorizationPolicy, RepositoryPermission +from vidxp.core.media import safe_media_suffix +from vidxp.idempotency import ( + IdempotencyKey, + scoped_job_id as derive_scoped_job_id, + scoped_request_key as derive_scoped_request_key, +) +from vidxp.ports import LocalFileResource + + +HttpIdempotencyKey = Annotated[ + IdempotencyKey, + Header( + alias="Idempotency-Key", + ), +] + + +def context(request: Request) -> HttpApplicationContext: + return request.app.state.vidxp + + +def principal( + request: Request, +) -> Principal: + active = request.scope.get("vidxp.principal") + if not isinstance(active, Principal): + raise ApplicationError( + "authentication_required", + ErrorCategory.authentication, + "Valid bearer authentication is required.", + ) + return active + + +def _authorized( + request: Request, + permission: RepositoryPermission, +) -> Principal: + active = principal(request) + service = context(request) + policy: AuthorizationPolicy = service.authorization + return policy.require(active, permission) + + +def read_principal( + request: Request, +) -> Principal: + return _authorized(request, RepositoryPermission.read) + + +def write_principal( + request: Request, +) -> Principal: + return _authorized(request, RepositoryPermission.write) + + +def accepted(response: Response, job: Job) -> Job: + response.headers["Location"] = f"/api/v1/jobs/{job.job_id}" + return job + + +def scoped_job_id( + service: HttpApplicationContext, + actor: Principal, + *, + operation: str, + idempotency_key: str, +) -> str: + """Derive a non-reversible DBOS workflow ID from an HTTP request key.""" + + return derive_scoped_job_id( + principal=actor, + transport="http", + operation=operation, + idempotency_key=idempotency_key, + ) + + +def scoped_request_key( + service: HttpApplicationContext, + actor: Principal, + *, + operation: str, + idempotency_key: str, +) -> str: + return derive_scoped_request_key( + principal=actor, + transport="http", + operation=operation, + idempotency_key=idempotency_key, + ) + + +def _etag_matches(request: Request, etag: str) -> bool: + supplied = request.headers.get("if-none-match") + if supplied is None: + return False + return any( + candidate == "*" or candidate.removeprefix("W/") == etag + for candidate in ( + item.strip() for item in supplied.split(",") + ) + ) + + +def file_response( + request: Request, + resource: LocalFileResource, + *, + disposition: str, +) -> Response: + etag = f'"{resource.etag}"' + common_headers = { + "ETag": etag, + "Cache-Control": "private, no-store", + "Accept-Ranges": "bytes", + } + if _etag_matches(request, etag): + return Response(status_code=304, headers=common_headers) + return FileResponse( + resource.path, + media_type=resource.mime_type, + filename=resource.filename, + headers=common_headers, + content_disposition_type=disposition, + ) + + +def copy_upload( + upload: UploadFile, + *, + maximum: int, +) -> Path: + suffix = safe_media_suffix(Path(upload.filename or "upload.bin")) + handle = tempfile.NamedTemporaryFile( + mode="w+b", + prefix="vidxp-upload-", + suffix=suffix, + delete=False, + ) + path = Path(handle.name) + total = 0 + try: + with handle: + while chunk := upload.file.read(1024 * 1024): + total += len(chunk) + if total > maximum: + raise ApplicationError( + "media_too_large", + ErrorCategory.resource_limit, + "The uploaded media exceeds the configured limit.", + ) + handle.write(chunk) + handle.flush() + os.fsync(handle.fileno()) + return path + except BaseException: + path.unlink(missing_ok=True) + raise diff --git a/src/vidxp/api_routes/index.py b/src/vidxp/api_routes/index.py new file mode 100644 index 0000000..84ee004 --- /dev/null +++ b/src/vidxp/api_routes/index.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends + +from vidxp.api_routes.dependencies import context, read_principal +from vidxp.application_models import IndexStatus +from vidxp.composition import HttpApplicationContext + + +router = APIRouter( + prefix="/index", + tags=["index"], + dependencies=[Depends(read_principal)], +) + + +@router.get( + "/status", + response_model=IndexStatus, + operation_id="getIndexStatus", + summary="Get index status", +) +def get_index_status( + service: Annotated[HttpApplicationContext, Depends(context)], +) -> IndexStatus: + return service.application.index_status() diff --git a/src/vidxp/api_routes/jobs.py b/src/vidxp/api_routes/jobs.py new file mode 100644 index 0000000..72e85c6 --- /dev/null +++ b/src/vidxp/api_routes/jobs.py @@ -0,0 +1,313 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Query, Response + +from vidxp.api_routes.dependencies import ( + accepted, + context, + HttpIdempotencyKey, + read_principal, + scoped_job_id, + write_principal, +) +from vidxp.application_models import ( + CreateActorOverlayCommand, + CreateIndexCommand, + CreateSnippetCommand, + Job, + JobPage, + JobResult, + ListJobsCommand, + Principal, + PrepareModelsCommand, + QueryVideoCommand, + SearchCommand, +) +from vidxp.composition import HttpApplicationContext +from vidxp.core.identifiers import JobId + + +router = APIRouter(prefix="/jobs", tags=["jobs"]) + + +@router.post( + "/index", + response_model=Job, + status_code=202, + operation_id="startIndexing", + summary="Start indexing", + description=( + "Add or replace one registered media item in the active multi-video " + "index snapshot." + ), + dependencies=[Depends(write_principal)], +) +def submit_index( + command: CreateIndexCommand, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(write_principal)], + idempotency_key: HttpIdempotencyKey, +) -> Job: + service.application.require_models(command.modalities) + return accepted( + response, + service.jobs.submit_index( + command, + job_id=scoped_job_id( + service, + actor, + operation="index", + idempotency_key=idempotency_key, + ), + ), + ) + + +@router.post( + "/search", + response_model=Job, + status_code=202, + operation_id="searchMoments", + summary="Search indexed moments", + description=( + "Set media_id to search one video, or omit it to rank moments across " + "every media item in the active index snapshot." + ), + dependencies=[Depends(read_principal)], +) +def submit_search( + command: SearchCommand, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(read_principal)], + idempotency_key: HttpIdempotencyKey, +) -> Job: + return accepted( + response, + service.jobs.submit_search( + command, + job_id=scoped_job_id( + service, + actor, + operation="search", + idempotency_key=idempotency_key, + ), + ), + ) + + +@router.post( + "/query", + response_model=Job, + status_code=202, + operation_id="queryVideo", + summary="Ask a grounded question about indexed media", + description=( + "Set media_id to ground the answer in one video, or omit it to use " + "evidence across every media item in the active index snapshot." + ), + dependencies=[Depends(read_principal)], +) +def submit_query( + command: QueryVideoCommand, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(read_principal)], + idempotency_key: HttpIdempotencyKey, +) -> Job: + return accepted( + response, + service.jobs.submit_query( + command, + job_id=scoped_job_id( + service, + actor, + operation="query", + idempotency_key=idempotency_key, + ), + ), + ) + + +@router.post( + "/snippet", + response_model=Job, + status_code=202, + operation_id="createSnippet", + summary="Create a downloadable video clip", + description=( + "Create a durable clip-rendering job from a media ID and time range " + "returned by search or query. Poll the job, then download the " + "resulting artifact through GET /api/v1/artifacts/{artifact_id}/content." + ), + dependencies=[Depends(write_principal)], +) +def submit_snippet( + command: CreateSnippetCommand, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(write_principal)], + idempotency_key: HttpIdempotencyKey, +) -> Job: + return accepted( + response, + service.jobs.submit_snippet( + command, + job_id=scoped_job_id( + service, + actor, + operation="snippet", + idempotency_key=idempotency_key, + ), + ), + ) + + +@router.post( + "/actor-overlay", + response_model=Job, + status_code=202, + operation_id="createActorOverlay", + summary="Create an actor overlay", + dependencies=[Depends(write_principal)], +) +def submit_actor_overlay( + command: CreateActorOverlayCommand, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(write_principal)], + idempotency_key: HttpIdempotencyKey, +) -> Job: + return accepted( + response, + service.jobs.submit_actor_overlay( + command, + job_id=scoped_job_id( + service, + actor, + operation="actor-overlay", + idempotency_key=idempotency_key, + ), + ), + ) + + +@router.post( + "/model-preparation", + response_model=Job, + status_code=202, + operation_id="prepareModels", + summary="Prepare models", + dependencies=[Depends(write_principal)], +) +def submit_model_preparation( + command: PrepareModelsCommand, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(write_principal)], + idempotency_key: HttpIdempotencyKey, +) -> Job: + return accepted( + response, + service.jobs.submit_prepare_models( + command, + job_id=scoped_job_id( + service, + actor, + operation="model-preparation", + idempotency_key=idempotency_key, + ), + ), + ) + + +@router.get( + "", + response_model=JobPage, + operation_id="listJobs", + summary="List jobs", + dependencies=[Depends(read_principal)], +) +def list_jobs( + service: Annotated[HttpApplicationContext, Depends(context)], + page_size: Annotated[int, Query(gt=0, le=100)] = 50, + cursor: Annotated[ + str | None, + Query(min_length=1, max_length=512), + ] = None, +) -> JobPage: + return service.jobs.list( + ListJobsCommand(page_size=page_size, cursor=cursor) + ) + + +@router.get( + "/{job_id}", + response_model=Job, + operation_id="getJob", + summary="Get a job", + dependencies=[Depends(read_principal)], +) +def get_job( + job_id: JobId, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> Job: + return service.jobs.get(job_id) + + +@router.get( + "/{job_id}/result", + response_model=JobResult, + operation_id="getJobResult", + summary="Get a job result", + dependencies=[Depends(read_principal)], +) +def get_job_result( + job_id: JobId, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> JobResult: + return service.jobs.result(job_id) + + +@router.post( + "/{job_id}/cancellation", + response_model=Job, + operation_id="cancelJob", + summary="Cancel a job", + dependencies=[Depends(write_principal)], +) +def cancel_job( + job_id: JobId, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> Job: + return service.jobs.cancel(job_id) + + +@router.post( + "/{job_id}/retries", + response_model=Job, + status_code=202, + operation_id="retryJob", + summary="Retry a job", + dependencies=[Depends(write_principal)], +) +def retry_job( + job_id: JobId, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(write_principal)], + idempotency_key: HttpIdempotencyKey, +) -> Job: + return accepted( + response, + service.jobs.retry( + job_id, + retry_id=scoped_job_id( + service, + actor, + operation=f"retry:{job_id}", + idempotency_key=idempotency_key, + ), + ), + ) diff --git a/src/vidxp/api_routes/media.py b/src/vidxp/api_routes/media.py new file mode 100644 index 0000000..0122bd2 --- /dev/null +++ b/src/vidxp/api_routes/media.py @@ -0,0 +1,240 @@ +from base64 import b64encode +from typing import Annotated + +from fastapi import APIRouter, Depends, File, Query, Request, Response, UploadFile + +from vidxp.api_routes.dependencies import ( + context, + copy_upload, + file_response, + HttpIdempotencyKey, + read_principal, + scoped_request_key, + write_principal, +) +from vidxp.application_models import ( + ApplicationError, + CreateUploadIntentCommand, + ErrorCategory, + ListMediaCommand, + MediaAsset, + MediaPage, + Principal, + UploadIntent, + UploadIntentId, +) +from vidxp.api_models import UploadIntentResponse +from vidxp.composition import HttpApplicationContext +from vidxp.core.identifiers import MediaId + + +router = APIRouter(prefix="/media", tags=["media"]) + + +def _upload_response( + service: HttpApplicationContext, + actor: Principal, + intent: UploadIntent, +) -> UploadIntentResponse: + if service.uploads is None: + raise ApplicationError( + "remote_upload_unavailable", + ErrorCategory.unavailable, + "Remote resumable uploads are not configured.", + ) + assert service.settings.upload_public_endpoint is not None + encoded_intent = b64encode(intent.intent_id.encode("ascii")).decode("ascii") + return UploadIntentResponse( + intent=intent, + creation_url=service.settings.upload_public_endpoint, + upload_metadata=f"intent_id {encoded_intent}", + resume_url=service.uploads.upload_url( + intent.intent_id, + principal=actor, + ), + ) + + +def _private_upload_response(response: Response) -> None: + response.headers["Cache-Control"] = "private, no-store" + response.headers["Referrer-Policy"] = "no-referrer" + + +@router.post( + "/uploads", + response_model=UploadIntentResponse, + status_code=201, + operation_id="createUploadIntent", + summary="Create a resumable media upload", + dependencies=[Depends(write_principal)], +) +def create_upload_intent( + command: CreateUploadIntentCommand, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(write_principal)], + idempotency_key: HttpIdempotencyKey, +) -> UploadIntentResponse: + if service.uploads is None: + raise ApplicationError( + "remote_upload_unavailable", + ErrorCategory.unavailable, + "Remote resumable uploads are not configured.", + ) + _private_upload_response(response) + intent = service.uploads.create_intent( + command, + principal=actor, + request_key=scoped_request_key( + service, + actor, + operation="media-upload-intent", + idempotency_key=idempotency_key, + ), + ) + response.headers["Location"] = f"/api/v1/media/uploads/{intent.intent_id}" + return _upload_response(service, actor, intent) + + +@router.get( + "/uploads/{intent_id}", + response_model=UploadIntentResponse, + operation_id="getUploadIntent", + summary="Get resumable upload state", + dependencies=[Depends(read_principal)], +) +def get_upload_intent( + intent_id: UploadIntentId, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(read_principal)], +) -> UploadIntentResponse: + if service.uploads is None: + raise ApplicationError( + "remote_upload_unavailable", + ErrorCategory.unavailable, + "Remote resumable uploads are not configured.", + ) + _private_upload_response(response) + return _upload_response( + service, + actor, + service.uploads.get_intent(intent_id, principal=actor), + ) + + +@router.post( + "", + response_model=MediaAsset, + status_code=201, + operation_id="importSmallMedia", + summary="Import a small media file", + dependencies=[Depends(write_principal)], +) +def import_media( + upload: Annotated[UploadFile, File()], + service: Annotated[HttpApplicationContext, Depends(context)], + actor: Annotated[Principal, Depends(write_principal)], + idempotency_key: HttpIdempotencyKey, +) -> MediaAsset: + staged = copy_upload( + upload, + maximum=service.settings.http_max_small_upload_bytes, + ) + try: + return service.application.import_uploaded_media( + staged_path=staged, + original_filename=upload.filename or "", + declared_mime_type=upload.content_type, + request_key=scoped_request_key( + service, + actor, + operation="media-import", + idempotency_key=idempotency_key, + ), + ) + finally: + staged.unlink(missing_ok=True) + + +@router.get( + "", + response_model=MediaPage, + operation_id="listMedia", + summary="List media", + description=( + "List registered filenames, metadata, and stable media IDs. A " + "registered item is searchable only when its ID is also present in " + "the active index snapshot." + ), + dependencies=[Depends(read_principal)], +) +def list_media( + service: Annotated[HttpApplicationContext, Depends(context)], + page_size: Annotated[int, Query(gt=0, le=100)] = 50, + cursor: Annotated[ + str | None, + Query(min_length=1, max_length=512), + ] = None, +) -> MediaPage: + return service.application.list_media( + ListMediaCommand(page_size=page_size, cursor=cursor) + ) + + +@router.get( + "/{media_id}", + response_model=MediaAsset, + operation_id="getMedia", + summary="Get media metadata", + description=( + "Get one registered media item by the stable ID returned from the " + "media list or a completed upload." + ), + dependencies=[Depends(read_principal)], +) +def get_media( + media_id: MediaId, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> MediaAsset: + return service.application.get_media(media_id) + + +def _content( + media_id: MediaId, + request: Request, + service: HttpApplicationContext, +) -> Response: + return file_response( + request, + service.application.open_media_content(media_id), + disposition="inline", + ) + + +@router.get( + "/{media_id}/content", + response_model=None, + operation_id="getMediaContent", + summary="Stream media content", + dependencies=[Depends(read_principal)], +) +def get_media_content( + media_id: MediaId, + request: Request, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> Response: + return _content(media_id, request, service) + + +@router.head( + "/{media_id}/content", + include_in_schema=False, + dependencies=[Depends(read_principal)], +) +def head_media_content( + media_id: MediaId, + request: Request, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> Response: + return _content(media_id, request, service) diff --git a/src/vidxp/api_routes/platform.py b/src/vidxp/api_routes/platform.py new file mode 100644 index 0000000..59fc8fa --- /dev/null +++ b/src/vidxp/api_routes/platform.py @@ -0,0 +1,54 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends + +from vidxp.api_routes.dependencies import context, read_principal +from vidxp.application_models import ( + CapabilityInfo, + CapabilityList, + RuntimeReadiness, +) +from vidxp.composition import HttpApplicationContext + + +router = APIRouter( + tags=["platform"], + dependencies=[Depends(read_principal)], +) + + +@router.get( + "/runtime/readiness", + response_model=RuntimeReadiness, + operation_id="getRuntimeReadiness", + summary="Inspect runtime readiness", +) +def runtime_readiness( + service: Annotated[HttpApplicationContext, Depends(context)], +) -> RuntimeReadiness: + return service.readiness.details() + + +@router.get( + "/capabilities", + response_model=CapabilityList, + operation_id="listCapabilities", + summary="List capabilities", +) +def list_capabilities( + service: Annotated[HttpApplicationContext, Depends(context)], +) -> CapabilityList: + return CapabilityList(items=service.application.list_capabilities()) + + +@router.get( + "/capabilities/{name}", + response_model=CapabilityInfo, + operation_id="getCapability", + summary="Get capability metadata", +) +def get_capability( + name: str, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> CapabilityInfo: + return service.application.get_capability(name) diff --git a/src/vidxp/cli.py b/src/vidxp/cli.py index 7135302..30a0985 100644 --- a/src/vidxp/cli.py +++ b/src/vidxp/cli.py @@ -9,21 +9,20 @@ import typer from vidxp import __version__ -from vidxp.application import VidXPService -from vidxp.capabilities.actor.results import ActorClusterNotFoundError -from vidxp.capabilities.registry import CAPABILITIES +from vidxp.application_models import ApplicationError +from vidxp.benchmarks.cli import app as benchmark_app +from vidxp.cli_commands.actors import app as actor_app from vidxp.cli_commands.index import app as index_app +from vidxp.cli_commands.jobs import app as jobs_app +from vidxp.cli_commands.mcp import mcp_config +from vidxp.cli_commands.media import app as media_app +from vidxp.cli_commands.artifacts import app as artifacts_app from vidxp.cli_commands.repositories import app as repositories_app -from vidxp.cli_commands.runtime import doctor, prepare, ui -from vidxp.cli_commands.search import app as search_app +from vidxp.cli_commands.runtime import doctor, initialize, prepare, ui +from vidxp.cli_commands.search import search +from vidxp.cli_commands.query import query from vidxp.cli_support import CLIState, OutputFormat -from vidxp.core.contracts import IndexSchemaError -from vidxp.dependencies import requirements_available -from vidxp.index_state import ( - IndexingInProgressError, - IndexNotReadyError, -) -from vidxp.repositories import resolve_repository +from vidxp.composition import create_local_application app = typer.Typer( @@ -31,27 +30,19 @@ help="Index and search video with installable capabilities.", ) app.add_typer(index_app, name="index") -app.add_typer(search_app, name="search") +app.add_typer(jobs_app, name="jobs") +app.add_typer(media_app, name="media") +app.add_typer(artifacts_app, name="artifacts") +app.command("search")(search) +app.command("query")(query) +app.command("mcp-config")(mcp_config) app.add_typer(repositories_app, name="repositories") +app.add_typer(actor_app, name="actors") -def _load_benchmark_app(): - if not requirements_available("vidxp.benchmarks"): - return None - from vidxp.benchmarks.cli import app as benchmark_app - - return benchmark_app - - -if _benchmark_app := _load_benchmark_app(): - app.add_typer(_benchmark_app, name="benchmark") -for _capability in CAPABILITIES.values(): - if _capability.cli_factory is not None: - app.add_typer( - _capability.cli_factory(), - name=_capability.cli_name, - ) +app.add_typer(benchmark_app, name="benchmark") app.command()(doctor) +app.command("init")(initialize) app.command()(prepare) app.command()(ui) @@ -99,6 +90,17 @@ def app_options( help="Override the selected repository index directory.", ), ] = None, + data_directory: Annotated[ + Path | None, + typer.Option( + "--data-dir", + file_okay=False, + help=( + "Store VidXP models and the default repository beneath this " + "directory." + ), + ), + ] = None, device: Annotated[ str | None, typer.Option( @@ -119,19 +121,20 @@ def app_options( typer.Option("--quiet", "-q", help="Suppress progress output."), ] = False, ) -> None: - registry, repository = resolve_repository( + if ctx.invoked_subcommand in {"init", "mcp-config"}: + return + local = create_local_application( registry_path=config_file, - name=repository_name, + repository_name=repository_name, index_directory=index_directory, + data_directory=data_directory, device=device, ) + ctx.call_on_close(local.close) ctx.obj = CLIState( - service=VidXPService( - repository.index_directory, - device=repository.device, - ), - registry=registry, - repository=repository, + local=local, + registry=local.repositories, + repository=local.repository, output_format=output_format, quiet=quiet, ) @@ -161,15 +164,20 @@ def _exit_code(exc: Exception) -> int: def _emit_error(exc: Exception, *, json_output: bool) -> None: message = _error_message(exc) if json_output: + error = ( + exc.to_dict() + if isinstance(exc, ApplicationError) + else { + "type": type(exc).__name__, + "message": message, + "exit_code": _exit_code(exc), + } + ) typer.echo( json.dumps( { "ok": False, - "error": { - "type": type(exc).__name__, - "message": message, - "exit_code": _exit_code(exc), - }, + "error": error, }, ensure_ascii=False, indent=2, @@ -185,30 +193,23 @@ def _emit_error(exc: Exception, *, json_output: bool) -> None: def main() -> None: try: - app(standalone_mode=False) + exit_code = app(standalone_mode=False) + if isinstance(exit_code, int) and exit_code: + raise SystemExit(exit_code) except typer.Exit as exc: raise SystemExit(exc.exit_code) from None except typer.Abort as exc: _emit_error(exc, json_output=_wants_json()) raise SystemExit(1) from exc except Exception as exc: + if isinstance(exc, ApplicationError): + _emit_error(exc, json_output=_wants_json()) + raise SystemExit(_exit_code(exc)) from exc is_command_error = hasattr(exc, "exit_code") and hasattr( exc, "format_message", ) - is_expected_runtime_error = isinstance( - exc, - ( - ActorClusterNotFoundError, - FileNotFoundError, - IndexNotReadyError, - IndexingInProgressError, - IndexSchemaError, - RuntimeError, - ValueError, - ), - ) - if not is_command_error and not is_expected_runtime_error: + if not is_command_error: raise _emit_error(exc, json_output=_wants_json()) raise SystemExit(_exit_code(exc)) from exc diff --git a/src/vidxp/capabilities/actor/cli.py b/src/vidxp/cli_commands/actors.py similarity index 62% rename from src/vidxp/capabilities/actor/cli.py rename to src/vidxp/cli_commands/actors.py index 51767ed..e89a4f8 100644 --- a/src/vidxp/capabilities/actor/cli.py +++ b/src/vidxp/cli_commands/actors.py @@ -1,21 +1,21 @@ from __future__ import annotations -from pathlib import Path from typing import Annotated, Iterable import typer from rich.console import Console from rich.table import Table +from vidxp.application_models import CreateActorOverlayCommand from vidxp.cli_support import ( CLIState, OutputFormat, effective_output_format, + emit_job_progress, emit_json, + emit_progress, state_from_context, ) - - app = typer.Typer( no_args_is_help=True, help="Inspect and render actor clusters.", @@ -30,7 +30,7 @@ def complete_cluster( if not isinstance(state, CLIState): return try: - clusters = state.service.actor_clusters() + clusters = state.service.actor_clusters(page_size=100).clusters except Exception: return for cluster in clusters: @@ -44,6 +44,14 @@ def complete_cluster( @app.command("list") def actors_list( ctx: typer.Context, + page_size: Annotated[ + int, + typer.Option("--page-size", min=1, max=100), + ] = 50, + cursor: Annotated[ + str | None, + typer.Option("--cursor", help="Cursor returned by the previous page."), + ] = None, json_output: Annotated[ bool, typer.Option("--json", help="Emit machine-readable JSON."), @@ -52,22 +60,30 @@ def actors_list( """List actor clusters in the selected index.""" state = state_from_context(ctx) - clusters = state.service.actor_clusters() + page = state.service.actor_clusters( + page_size=page_size, + cursor=cursor, + ) + clusters = page.clusters payload = { "clusters": [cluster.to_dict() for cluster in clusters], "count": len(clusters), + "total": page.total, + "next_cursor": page.next_cursor, } if effective_output_format(state, json_output) == OutputFormat.json: emit_json(payload) return table = Table(title="Actor clusters") table.add_column("Cluster") + table.add_column("Media") table.add_column("Detections", justify="right") table.add_column("First", justify="right") table.add_column("Last", justify="right") for cluster in clusters: table.add_row( cluster.cluster_id, + cluster.media_id, str(cluster.detection_count), f"{cluster.first_timestamp:.3f}s", f"{cluster.last_timestamp:.3f}s", @@ -89,6 +105,10 @@ def actors_inspect( int, typer.Option("--limit", min=1, help="Maximum detections to display."), ] = 20, + cursor: Annotated[ + str | None, + typer.Option("--cursor", help="Cursor returned by the previous page."), + ] = None, json_output: Annotated[ bool, typer.Option("--json", help="Emit machine-readable JSON."), @@ -97,17 +117,21 @@ def actors_inspect( """Inspect retained detections for one actor cluster.""" state = state_from_context(ctx) - detections = state.service.actor_detections(cluster_id) - if not detections: - raise typer.BadParameter(f"Actor cluster {cluster_id} was not found.") + page = state.service.actor_detections( + cluster_id, + page_size=min(limit, 100), + cursor=cursor, + ) + detections = page.detections payload = { "cluster_id": cluster_id, - "detection_count": len(detections), + "detection_count": page.total, "detections": [ detection.model_dump(mode="json") - for detection in detections[:limit] + for detection in detections ], - "truncated": len(detections) > limit, + "truncated": page.next_cursor is not None, + "next_cursor": page.next_cursor, } if effective_output_format(state, json_output) == OutputFormat.json: emit_json(payload) @@ -116,15 +140,17 @@ def actors_inspect( table.add_column("Frame", justify="right") table.add_column("Timestamp", justify="right") table.add_column("Detection") - for detection in detections[:limit]: + for detection in detections: table.add_row( str(detection.frame_index), f"{detection.timestamp:.3f}s", detection.detection_id, ) Console().print(table) - if len(detections) > limit: - typer.echo(f"Showing {limit} of {len(detections)} detections.") + if page.next_cursor is not None: + typer.echo( + f"Showing {len(detections)} detections; more are available." + ) @app.command("render") @@ -137,47 +163,46 @@ def actors_render( help="Actor cluster identifier.", ), ], - input_path: Annotated[ - Path, - typer.Argument( - exists=True, - dir_okay=False, - readable=True, - resolve_path=True, - help="Source video used to create the active index.", - ), - ], - output_path: Annotated[ - Path, - typer.Option( - "--output", - "-o", - dir_okay=False, - help="Rendered video destination.", - ), - ] = Path("output.mp4"), json_output: Annotated[ bool, typer.Option("--json", help="Emit machine-readable JSON."), ] = False, + detach: Annotated[ + bool, + typer.Option( + "--detach", + help="Return after the durable job is queued.", + ), + ] = False, ) -> None: """Render one actor cluster as a result video.""" state = state_from_context(ctx) - result = state.service.render_actor( - cluster_id, - input_path, - output_path, + output_format = effective_output_format(state, json_output) + show_progress = ( + not detach + and not state.quiet + and output_format == OutputFormat.rich ) - payload = { - "cluster_id": cluster_id, - "output_path": str(result.output_path), - "detection_count": result.detection_count, - } - if effective_output_format(state, json_output) == OutputFormat.json: + if show_progress: + emit_progress("Starting actor-overlay rendering...") + job = state.jobs.submit_actor_overlay( + CreateActorOverlayCommand(cluster_id=cluster_id) + ) + if not detach: + job = state.jobs.wait( + job.job_id, + progress=emit_job_progress if show_progress else None, + ) + payload = job.model_dump(mode="json") + if output_format == OutputFormat.json: emit_json(payload) else: typer.secho( - f"Video saved as {result.output_path}", + ( + f"Actor overlay job queued: {job.job_id}" + if detach + else f"Actor overlay job completed: {job.job_id}" + ), fg=typer.colors.GREEN, ) diff --git a/src/vidxp/cli_commands/artifacts.py b/src/vidxp/cli_commands/artifacts.py new file mode 100644 index 0000000..8d590ff --- /dev/null +++ b/src/vidxp/cli_commands/artifacts.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Annotated +from uuid import uuid4 + +import typer + +from vidxp.application_models import ( + ArtifactJobResult, + CreateSnippetCommand, + SnippetProfile, +) +from vidxp.cli_support import ( + OutputFormat, + effective_output_format, + emit_job_progress, + emit_json, + emit_progress, + require_media_runtime, + state_from_context, +) + + +app = typer.Typer(no_args_is_help=True, help="Inspect generated artifacts.") + + +@app.command("snippet") +def create_snippet( + ctx: typer.Context, + media_id: Annotated[ + str, + typer.Argument(help="Cataloged media identifier."), + ], + start_seconds: Annotated[ + float, + typer.Argument(min=0, help="Snippet start in seconds."), + ], + end_seconds: Annotated[ + float, + typer.Argument(min=0, help="Snippet end in seconds."), + ], + profile: Annotated[ + SnippetProfile, + typer.Option(help="Output compatibility profile."), + ] = SnippetProfile.compatible_mp4, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, + detach: Annotated[ + bool, + typer.Option( + "--detach", + help="Return after the durable job is queued.", + ), + ] = False, +) -> None: + """Create a managed video snippet artifact.""" + + if end_seconds <= start_seconds: + raise typer.BadParameter( + "The snippet end must be greater than its start.", + param_hint="end_seconds", + ) + require_media_runtime() + state = state_from_context(ctx) + output_format = effective_output_format(state, json_output) + show_progress = ( + not detach + and not state.quiet + and output_format == OutputFormat.rich + ) + if show_progress: + emit_progress("Starting snippet rendering...") + job = state.jobs.submit_snippet( + CreateSnippetCommand( + media_id=media_id, + start_seconds=start_seconds, + end_seconds=end_seconds, + profile=profile, + ) + ) + if not detach: + job = state.jobs.wait( + job.job_id, + progress=emit_job_progress if show_progress else None, + ) + payload = job.model_dump(mode="json") + if output_format == OutputFormat.json: + emit_json(payload) + else: + if detach: + typer.secho( + f"Snippet job queued: {job.job_id}", + fg=typer.colors.GREEN, + ) + elif isinstance(job.result, ArtifactJobResult): + artifact = job.result.result + typer.secho( + f"Clip ready: {artifact.artifact_id} " + f"({artifact.byte_size:,} bytes)", + fg=typer.colors.GREEN, + ) + typer.echo( + "Download it with: " + f"vidxp artifacts download {artifact.artifact_id}" + ) + else: + typer.secho( + f"Snippet artifact job completed: {job.job_id}", + fg=typer.colors.GREEN, + ) + + +@app.command("show") +def show_artifact( + ctx: typer.Context, + artifact_id: Annotated[ + str, + typer.Argument(help="Artifact identifier."), + ], + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Show metadata for one generated artifact.""" + + state = state_from_context(ctx) + artifact = state.service.get_artifact(artifact_id) + payload = artifact.model_dump(mode="json") + if effective_output_format(state, json_output) == OutputFormat.json: + emit_json(payload) + else: + typer.echo( + f"{artifact.kind.value} {artifact.artifact_id} " + f"({artifact.byte_size:,} bytes)" + ) + + +@app.command("download") +def download_artifact( + ctx: typer.Context, + artifact_id: Annotated[ + str, + typer.Argument(help="Artifact identifier from a completed job."), + ], + destination: Annotated[ + Path | None, + typer.Argument( + help=( + "Output file or existing directory. Defaults to the generated " + "artifact filename in the current directory." + ), + ), + ] = None, + force: Annotated[ + bool, + typer.Option( + "--force", + help="Replace an existing output file.", + ), + ] = False, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Download a generated clip or artifact to a local file.""" + + state = state_from_context(ctx) + resource = state.service.open_artifact_content(artifact_id) + output = destination or Path(resource.filename) + if output.is_dir(): + output = output / resource.filename + output = output.expanduser().resolve() + if not output.parent.is_dir(): + raise typer.BadParameter( + "The output directory does not exist.", + param_hint="destination", + ) + if output.exists() and not force: + raise typer.BadParameter( + "The output file already exists; pass --force to replace it.", + param_hint="destination", + ) + + temporary = output.with_name(f".{output.name}.{uuid4().hex}.tmp") + try: + shutil.copyfile(resource.path, temporary) + if output.exists() and not force: + raise typer.BadParameter( + "The output file already exists; pass --force to replace it.", + param_hint="destination", + ) + temporary.replace(output) + finally: + temporary.unlink(missing_ok=True) + + payload = { + "artifact_id": artifact_id, + "path": str(output), + "mime_type": resource.mime_type, + "byte_size": resource.byte_size, + "sha256": resource.etag, + } + if effective_output_format(state, json_output) == OutputFormat.json: + emit_json(payload) + else: + typer.secho( + f"Downloaded {resource.byte_size:,} bytes to {output}", + fg=typer.colors.GREEN, + ) diff --git a/src/vidxp/cli_commands/index.py b/src/vidxp/cli_commands/index.py index f552757..8d0036a 100644 --- a/src/vidxp/cli_commands/index.py +++ b/src/vidxp/cli_commands/index.py @@ -1,10 +1,15 @@ from __future__ import annotations -from pathlib import Path from typing import Annotated, Iterable import typer +from rich.console import Console +from rich.table import Table +from vidxp.application_models import ( + CreateIndexCommand, + RemoveIndexCommand, +) from vidxp.cli_support import ( CLIState, IndexProgress, @@ -23,28 +28,50 @@ def create_index( state: CLIState, - path: Path, + media_id: str, *, modalities: Iterable[str], frame_stride: int, + scene_sample_fps: float | None, capability_options: dict[str, dict], + detach: bool = False, ) -> dict: show_progress = ( not state.quiet and state.output_format == OutputFormat.rich ) + selected = tuple(modalities) + state.service.require_models(selected) with IndexProgress(show_progress) as progress: - summary = state.service.create_index( - path, - modalities=modalities, - frame_stride=frame_stride, - capability_options=capability_options, - progress_callback=progress.update, + job = state.jobs.submit_index( + CreateIndexCommand( + media_id=media_id, + modalities=selected, + frame_stride=frame_stride, + scene_sample_fps=scene_sample_fps, + capability_options=capability_options, + ), ) + if not detach: + job = state.jobs.wait( + job.job_id, + progress=lambda current: ( + progress.update( + current.progress.model_dump(mode="python") + ) + if current.progress is not None + else None + ), + ) + summary = job.model_dump(mode="json") if state.output_format == OutputFormat.json: emit_json(summary) else: typer.secho( - "Video indexing completed successfully.", + ( + f"Indexing job queued: {job.job_id}" + if detach + else f"Video indexing completed: {job.job_id}" + ), fg=typer.colors.GREEN, bold=True, ) @@ -54,15 +81,9 @@ def create_index( @app.command("create") def index_create( ctx: typer.Context, - path: Annotated[ - Path, - typer.Argument( - exists=True, - dir_okay=False, - readable=True, - resolve_path=True, - help="Local video file to index.", - ), + media_id: Annotated[ + str, + typer.Argument(help="Registered media identifier to index."), ], modalities: Annotated[ list[str] | None, @@ -77,9 +98,23 @@ def index_create( typer.Option( "--frame-stride", min=1, - help="Materialize every Nth frame for visual modalities.", + help=( + "Materialize every Nth frame for actor and legacy visual " + "indexing." + ), ), ] = 1, + scene_sample_fps: Annotated[ + float | None, + typer.Option( + "--scene-sample-fps", + min=0.01, + help=( + "Target scene samples per second; lower-FPS media uses every " + "available frame." + ), + ), + ] = None, capability_options: Annotated[ list[str] | None, typer.Option( @@ -90,19 +125,65 @@ def index_create( ), ), ] = None, + detach: Annotated[ + bool, + typer.Option( + "--detach", + help="Return after the durable job is queued.", + ), + ] = False, ) -> None: - """Create or replace a local index for one video.""" + """Add media or replace its immutable generation in the active index.""" state = state_from_context(ctx) + indexable = tuple( + capability.name + for capability in state.service.list_capabilities() + if capability.supports_indexing + ) create_index( state, - path, - modalities=selected_modalities(modalities), + media_id=media_id, + modalities=selected_modalities( + modalities, + indexable, + ), frame_stride=frame_stride, + scene_sample_fps=scene_sample_fps, capability_options=parse_capability_options(capability_options), + detach=detach, ) +@app.command("remove") +def index_remove( + ctx: typer.Context, + media_id: Annotated[ + str, + typer.Argument(help="Media identifier to remove from the active index."), + ], + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Remove one media item from the active snapshot.""" + + state = state_from_context(ctx) + removed = state.service.remove_from_index( + RemoveIndexCommand(media_id=media_id) + ) + payload = {"removed": removed, "media_id": media_id} + if effective_output_format(state, json_output) == OutputFormat.json: + emit_json(payload) + else: + typer.echo( + "Media removed." + if removed + else "The media identifier was not in the active index." + ) + + @app.command("status") def index_status( ctx: typer.Context, @@ -115,11 +196,75 @@ def index_status( state = state_from_context(ctx) emit_status( - state.service.index_status(), + state.service.index_status().model_dump(mode="json"), output_format=effective_output_format(state, json_output), ) +@app.command("list") +def index_list( + ctx: typer.Context, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """List registered metadata for media in the active index snapshot.""" + + state = state_from_context(ctx) + status = state.service.index_status() + summary = status.summary + assets = ( + () + if summary is None + else tuple( + state.service.get_media(media_id) + for media_id in summary.media_ids + ) + ) + payload = { + "state": status.state, + "message": status.message, + "snapshot_id": None if summary is None else summary.snapshot_id, + "media_count": 0 if summary is None else summary.media_count, + "media_ids_truncated": ( + False if summary is None else summary.media_ids_truncated + ), + "modalities": [] if summary is None else list(summary.modalities), + "items": [asset.model_dump(mode="json") for asset in assets], + } + if effective_output_format(state, json_output) == OutputFormat.json: + emit_json(payload) + return + if summary is None: + typer.echo(status.message) + return + + table = Table(title="Active index media") + table.add_column("ID") + table.add_column("Filename") + table.add_column("Duration", justify="right") + table.add_column("Size", justify="right") + for asset in assets: + table.add_row( + asset.media_id, + asset.original_filename, + f"{asset.duration_seconds:.3f}s", + f"{asset.byte_size:,}", + ) + Console().print(table) + typer.echo( + f"Snapshot {summary.snapshot_id}: {summary.media_count} media item(s); " + f"modalities: {', '.join(summary.modalities) or 'none'}." + ) + if summary.media_ids_truncated: + typer.secho( + "The active snapshot is larger than this status page; some media " + "items are not shown.", + fg=typer.colors.YELLOW, + ) + + @app.command("clear") def index_clear( ctx: typer.Context, @@ -132,18 +277,17 @@ def index_clear( typer.Option("--json", help="Emit machine-readable JSON."), ] = False, ) -> None: - """Clear indexed records and VidXP run state.""" + """Publish an empty active snapshot without deleting retained generations.""" state = state_from_context(ctx) if not yes: typer.confirm( - f"Clear the local index at {state.service.index_directory}?", + f"Clear the active index at {state.service.index_directory}?", abort=True, ) cleared = state.service.clear_index() payload = { "cleared": cleared, - "index_directory": str(state.service.index_directory), } if effective_output_format(state, json_output) == OutputFormat.json: emit_json(payload) diff --git a/src/vidxp/cli_commands/jobs.py b/src/vidxp/cli_commands/jobs.py new file mode 100644 index 0000000..b7181e9 --- /dev/null +++ b/src/vidxp/cli_commands/jobs.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from typing import Annotated + +import typer +from rich.console import Console +from rich.table import Table + +from vidxp.application_models import ListJobsCommand +from vidxp.cli_support import ( + OutputFormat, + effective_output_format, + emit_json, + state_from_context, +) + + +app = typer.Typer(no_args_is_help=True, help="Manage durable background jobs.") + + +@app.command("list") +def list_jobs( + ctx: typer.Context, + page_size: Annotated[ + int, + typer.Option("--page-size", min=1, max=100), + ] = 50, + cursor: Annotated[ + str | None, + typer.Option("--cursor", help="Cursor returned by the previous page."), + ] = None, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """List durable VidXP jobs.""" + + state = state_from_context(ctx) + page = state.jobs.list( + ListJobsCommand(page_size=page_size, cursor=cursor) + ) + payload = page.model_dump(mode="json") + if effective_output_format(state, json_output) == OutputFormat.json: + emit_json(payload) + return + table = Table(title="VidXP jobs") + table.add_column("Job") + table.add_column("Kind") + table.add_column("State") + table.add_column("Queue") + table.add_column("Progress") + for job in page.items: + table.add_row( + job.job_id, + job.kind.value, + job.state.value, + job.queue.value, + job.progress.message if job.progress is not None else "—", + ) + Console().print(table) + if page.next_cursor is not None: + typer.echo(f"Next cursor: {page.next_cursor}") + + +@app.command("show") +def show_job( + ctx: typer.Context, + job_id: Annotated[str, typer.Argument(help="Durable job identifier.")], +) -> None: + """Show one job and its typed result when available.""" + + state = state_from_context(ctx) + emit_json(state.jobs.get(job_id).model_dump(mode="json")) + + +@app.command("cancel") +def cancel_job( + ctx: typer.Context, + job_id: Annotated[str, typer.Argument(help="Durable job identifier.")], +) -> None: + """Request cancellation of a queued or running job.""" + + state = state_from_context(ctx) + emit_json(state.jobs.cancel(job_id).model_dump(mode="json")) + + +@app.command("retry") +def retry_job( + ctx: typer.Context, + job_id: Annotated[str, typer.Argument(help="Durable job identifier.")], +) -> None: + """Retry a failed or cancelled job through DBOS recovery.""" + + state = state_from_context(ctx) + emit_json(state.jobs.retry(job_id).model_dump(mode="json")) + + +@app.command("stop-worker") +def stop_worker(ctx: typer.Context) -> None: + """Stop the detached local worker; durable jobs remain recoverable.""" + + state = state_from_context(ctx) + stopped = state.jobs.stop_worker() + emit_json({"stopped": stopped}) diff --git a/src/vidxp/cli_commands/mcp.py b/src/vidxp/cli_commands/mcp.py new file mode 100644 index 0000000..5ef773a --- /dev/null +++ b/src/vidxp/cli_commands/mcp.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Annotated + +import typer + +from vidxp.mcp_cli import render_stdio_client_config + + +def mcp_config( + registry: Annotated[ + Path | None, + typer.Option( + "--registry", + dir_okay=False, + help="Path to the named-repository configuration file.", + ), + ] = None, + repository: Annotated[ + str, + typer.Option( + "--repository", + "-r", + help="Repository the local MCP server should use.", + ), + ] = "default", + index_directory: Annotated[ + Path | None, + typer.Option( + "--index-directory", + file_okay=False, + help="Override the selected repository's index directory.", + ), + ] = None, + data_directory: Annotated[ + Path | None, + typer.Option( + "--data-dir", + file_okay=False, + help="Store VidXP models and the default repository here.", + ), + ] = None, + device: Annotated[ + str | None, + typer.Option( + "--device", + help="Override the selected repository runtime device.", + ), + ] = None, +) -> None: + """Print copy/paste JSON for a local stdio MCP client.""" + + typer.echo( + render_stdio_client_config( + registry=None if registry is None else str(registry), + repository=repository, + index_directory=( + None if index_directory is None else str(index_directory) + ), + data_directory=data_directory, + device=device, + ) + ) diff --git a/src/vidxp/cli_commands/media.py b/src/vidxp/cli_commands/media.py new file mode 100644 index 0000000..83f2746 --- /dev/null +++ b/src/vidxp/cli_commands/media.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Annotated + +import typer +from rich.console import Console +from rich.table import Table + +from vidxp.application_models import ImportMediaCommand, ListMediaCommand +from vidxp.cli_support import ( + OutputFormat, + effective_output_format, + emit_json, + emit_progress, + require_media_runtime, + state_from_context, +) + + +app = typer.Typer(no_args_is_help=True, help="Import and inspect local media.") + + +@app.command("import") +def import_media( + ctx: typer.Context, + path: Annotated[ + Path, + typer.Argument( + exists=True, + dir_okay=False, + readable=True, + resolve_path=True, + help="Local video file to copy into managed storage.", + ), + ], + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Validate and register a local video, returning its stable media ID.""" + + require_media_runtime() + state = state_from_context(ctx) + output_format = effective_output_format(state, json_output) + if not state.quiet and output_format == OutputFormat.rich: + emit_progress(f"Importing {path.name} into managed storage...") + result = state.service.import_media(ImportMediaCommand(path=path)) + payload = result.model_dump(mode="json") + if output_format == OutputFormat.json: + emit_json(payload) + else: + typer.secho( + f"Imported {result.original_filename} as {result.media_id}", + fg=typer.colors.GREEN, + ) + + +@app.command("list") +def list_media( + ctx: typer.Context, + limit: Annotated[ + int, + typer.Option("--limit", min=1, max=100), + ] = 100, + cursor: Annotated[ + str | None, + typer.Option("--cursor", help="Cursor returned by the previous page."), + ] = None, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """List filenames, metadata, and stable IDs used by other commands.""" + + state = state_from_context(ctx) + page = state.service.list_media( + ListMediaCommand(page_size=limit, cursor=cursor) + ) + assets = page.items + payload = page.model_dump(mode="json") + if effective_output_format(state, json_output) == OutputFormat.json: + emit_json(payload) + return + table = Table(title="Media") + table.add_column("ID") + table.add_column("Filename") + table.add_column("Duration", justify="right") + table.add_column("Size", justify="right") + for asset in assets: + table.add_row( + asset.media_id, + asset.original_filename, + f"{asset.duration_seconds:.3f}s", + f"{asset.byte_size:,}", + ) + Console().print(table) + + +@app.command("show") +def show_media( + ctx: typer.Context, + media_id: Annotated[ + str, + typer.Argument( + help="Stable media identifier returned by import or list." + ), + ], + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Show all registered metadata for one media ID.""" + + state = state_from_context(ctx) + payload = state.service.get_media(media_id).model_dump(mode="json") + if effective_output_format(state, json_output) == OutputFormat.json: + emit_json(payload) + else: + Console().print_json(data=payload) diff --git a/src/vidxp/cli_commands/query.py b/src/vidxp/cli_commands/query.py new file mode 100644 index 0000000..36f360a --- /dev/null +++ b/src/vidxp/cli_commands/query.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from typing import Annotated + +import typer + +from vidxp.application_models import ( + QueryAnswer, + QueryJobResult, + QueryVideoCommand, +) +from vidxp.cli_support import ( + CLIState, + OutputFormat, + effective_output_format, + emit_job_progress, + emit_progress, + emit_query, + state_from_context, +) + + +def run_query( + state: CLIState, + question: str, + *, + media_id: str | None, + modalities: tuple[str, ...], + top_k: int, + json_output: bool, +) -> QueryAnswer: + output_format = effective_output_format(state, json_output) + show_progress = not state.quiet and output_format == OutputFormat.rich + if show_progress: + emit_progress("Starting grounded video query...") + job = state.jobs.submit_query( + QueryVideoCommand( + question=question, + media_id=media_id, + modalities=modalities, + top_k=top_k, + ) + ) + completed = state.jobs.wait( + job.job_id, + progress=emit_job_progress if show_progress else None, + ) + if not isinstance(completed.result, QueryJobResult): + raise RuntimeError("The completed query job has no query result.") + result = completed.result.result + emit_query( + result, + output_format=output_format, + ) + return result + + +def query( + ctx: typer.Context, + question: Annotated[ + str, + typer.Argument(help="Natural-language question about indexed media."), + ], + media_id: Annotated[ + str | None, + typer.Option( + "--media-id", + help=( + "Use evidence only from this media ID. Omit to query every " + "media item in the active index snapshot." + ), + ), + ] = None, + modality: Annotated[ + list[str] | None, + typer.Option( + "--modality", + "-m", + help="Restrict query evidence; repeat for multiple capabilities.", + ), + ] = None, + top_k: Annotated[ + int, + typer.Option("--top-k", "-k", min=1, max=50), + ] = 10, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + run_query( + state_from_context(ctx), + question, + media_id=media_id, + modalities=tuple(dict.fromkeys(modality or ())), + top_k=top_k, + json_output=json_output, + ) diff --git a/src/vidxp/cli_commands/runtime.py b/src/vidxp/cli_commands/runtime.py index 94a97e6..f5992bc 100644 --- a/src/vidxp/cli_commands/runtime.py +++ b/src/vidxp/cli_commands/runtime.py @@ -1,38 +1,197 @@ from __future__ import annotations -import os +import sys +from pathlib import Path from typing import Annotated import typer -from vidxp.capabilities.registry import CAPABILITIES, capability_names +from vidxp.app_paths import available_storage_bytes +from vidxp.application_models import ( + ApplicationError, + CapabilityDependencyCheck, + DependencyCheckCommand, + DependencyKind, + ErrorCategory, + PrepareModelsCommand, +) from vidxp.cli_support import ( OutputFormat, effective_output_format, + emit_job_progress, emit_json, + emit_progress, parse_capability_options, parse_modalities, + require_media_runtime, state_from_context, ) - -ALL_CAPABILITIES = ",".join(capability_names()) -PREPARABLE_CAPABILITIES = ",".join( - name - for name, capability in CAPABILITIES.items() - if capability.prepare is not None +from vidxp.media_runtime import ( + MediaRuntimeStatus, + inspect_media_runtime, + install_media_runtime, + media_runtime_config_path, + save_media_runtime_configuration, ) +def _format_bytes(size: int) -> str: + gib = 1024**3 + mib = 1024**2 + if size >= gib: + return f"{size / gib:.2f} GiB" + return f"{size / mib:.1f} MiB" + + +def _media_runtime_payload(status: MediaRuntimeStatus) -> dict: + payload = status.model_dump(mode="json") + if status.install_plan is not None: + payload["install_command"] = status.install_plan.display_command + else: + payload["install_command"] = None + return payload + + +def initialize( + ctx: typer.Context, + ffmpeg: Annotated[ + Path | None, + typer.Option( + exists=True, + dir_okay=False, + resolve_path=True, + help="Use this FFmpeg executable instead of searching PATH.", + ), + ] = None, + ffprobe: Annotated[ + Path | None, + typer.Option( + exists=True, + dir_okay=False, + resolve_path=True, + help="Use this ffprobe executable instead of searching PATH.", + ), + ] = None, + yes: Annotated[ + bool, + typer.Option( + "--yes", + "-y", + help="Confirm the displayed system package-manager command.", + ), + ] = False, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + """Verify and save the system FFmpeg runtime used by VidXP.""" + + root_format = ctx.find_root().params.get( + "output_format", + OutputFormat.rich, + ) + output_format = ( + OutputFormat.json + if json_output or root_format == OutputFormat.json + else OutputFormat.rich + ) + status = inspect_media_runtime(ffmpeg=ffmpeg, ffprobe=ffprobe) + if not status.ready: + payload = _media_runtime_payload(status) + if output_format == OutputFormat.rich: + typer.secho( + "FFmpeg and ffprobe are not ready.", + fg=typer.colors.YELLOW, + bold=True, + ) + for error in status.errors: + typer.echo(f" {error}") + if status.install_plan is not None: + typer.echo( + f"Package manager: {status.install_plan.manager}" + ) + typer.secho( + f"Command: {status.install_plan.display_command}", + fg=typer.colors.YELLOW, + ) + plan = status.install_plan + if plan is None or not plan.automatic: + if output_format == OutputFormat.json: + emit_json(payload) + elif plan is None: + typer.echo( + "Install FFmpeg and ffprobe with your operating-system " + "package manager, or pass explicit --ffmpeg and " + "--ffprobe paths, then rerun `vidxp init`." + ) + else: + typer.echo( + "Install FFmpeg with the command above, or pass explicit " + "--ffmpeg and --ffprobe paths, then rerun `vidxp init`." + ) + raise typer.Exit(1) + interactive = sys.stdin.isatty() and sys.stdout.isatty() + if not yes: + if not interactive: + if output_format == OutputFormat.json: + emit_json(payload) + else: + typer.echo( + "No installation was started. Rerun interactively or " + "pass --yes after reviewing the command." + ) + raise typer.Exit(1) + typer.confirm( + f"Install through {plan.manager} using the displayed command?", + abort=True, + ) + if output_format == OutputFormat.rich: + emit_progress(f"Installing FFmpeg through {plan.manager}...") + install_media_runtime( + plan, + output_to_stderr=output_format == OutputFormat.json, + ) + status = inspect_media_runtime(ffmpeg=ffmpeg, ffprobe=ffprobe) + if not status.ready: + if output_format == OutputFormat.json: + emit_json(_media_runtime_payload(status)) + else: + for error in status.errors: + typer.secho(error, fg=typer.colors.RED) + raise typer.Exit(1) + + configuration = save_media_runtime_configuration(status) + payload = { + **_media_runtime_payload(status), + "initialized": True, + "configuration": str(media_runtime_config_path()), + } + if output_format == OutputFormat.json: + emit_json(payload) + else: + typer.secho( + "VidXP media runtime is initialized.", + fg=typer.colors.GREEN, + bold=True, + ) + typer.echo(f"FFmpeg: {configuration.ffmpeg_executable}") + typer.echo(f"ffprobe: {configuration.ffprobe_executable}") + + def doctor( ctx: typer.Context, modalities: Annotated[ - str, + list[str] | None, typer.Option( "--modalities", "-m", - help="Only validate dependencies for these modalities.", + help=( + "Only validate dependencies for these modalities. " + "Accepts comma-separated values or repeated options." + ), ), - ] = ALL_CAPABILITIES, + ] = None, json_output: Annotated[ bool, typer.Option("--json", help="Emit machine-readable JSON."), @@ -40,29 +199,165 @@ def doctor( ) -> None: """Validate selected indexing dependencies without downloading models.""" - selected = parse_modalities(modalities) state = state_from_context(ctx) - result = state.service.check_dependencies(selected) - if effective_output_format(state, json_output) == OutputFormat.json: - emit_json(result) + capabilities = tuple(state.service.list_capabilities()) + available = tuple(capability.name for capability in capabilities) + selected = ( + available + if modalities is None + else parse_modalities(",".join(modalities), available) + ) + output_format = effective_output_format(state, json_output) + + def show_check_start( + capability: str, + kind: DependencyKind, + name: str, + ) -> None: + label = { + DependencyKind.distribution: f"package {name}", + DependencyKind.model: f"model {name}", + }.get(kind, name) + emit_progress( + f"Checking [{capability}] {label}...", + newline=False, + ) + + def show_check_complete( + check: CapabilityDependencyCheck, + elapsed_seconds: float, + ) -> None: + if check.ok: + detail = ( + f"version {check.installed_version}, {elapsed_seconds:.1f}s" + if check.kind == DependencyKind.distribution + else f"{elapsed_seconds:.1f}s" + ) + typer.secho( + f" OK ({detail})", + fg=typer.colors.GREEN, + ) + else: + typer.secho( + f" FAILED ({elapsed_seconds:.1f}s): {check.error}", + fg=typer.colors.RED, + ) + + result = state.service.check_dependencies( + DependencyCheckCommand( + modalities=selected, + include_models=True, + ), + on_check_start=( + show_check_start if output_format == OutputFormat.rich else None + ), + on_check_complete=( + show_check_complete + if output_format == OutputFormat.rich + else None + ), + ) + payload = result.model_dump(mode="json") + if output_format == OutputFormat.json: + emit_json(payload) else: - for check in result["checks"]: - if check["ok"]: - detail = f": {check['path']}" if check.get("path") else "" + python_failures = tuple( + check + for check in result.checks + if ( + not check.ok + and check.capability != "media" + and check.kind != DependencyKind.model + ) + ) + if python_failures: + selected_capabilities = { + capability.name: capability for capability in capabilities + } + builtin_extras = tuple( + dict.fromkeys( + selected_capabilities[name].install_extra + for name in selected + if selected_capabilities[name].provenance is None + ) + ) + external_distributions = tuple( + dict.fromkeys( + selected_capabilities[name].provenance.distribution + for name in selected + if selected_capabilities[name].provenance is not None + ) + ) + if builtin_extras: typer.secho( - f"OK {check['name']}{detail}", - fg=typer.colors.GREEN, + "REMEDY: In a source checkout, rerun uv sync with all " + "profiles you use and include --extra local-worker.", + fg=typer.colors.YELLOW, ) - else: typer.secho( - f"FAILED {check['name']}: {check['error']}", - fg=typer.colors.RED, + 'Published package: pip install "vidxp[' + + ",".join(builtin_extras) + + ']"', + fg=typer.colors.YELLOW, ) - if not result["ok"]: + if external_distributions: + typer.secho( + "External capabilities: pip install " + + " ".join(external_distributions), + fg=typer.colors.YELLOW, + ) + media_failures = tuple( + check + for check in result.checks + if not check.ok and check.capability == "media" + ) + if media_failures: + typer.secho( + "REMEDY: Run `vidxp init` to verify and configure FFmpeg.", + fg=typer.colors.YELLOW, + ) + missing_model_checks = tuple( + check + for check in result.checks + if not check.ok and check.kind == DependencyKind.model + ) + missing_models = tuple( + dict.fromkeys(check.capability for check in missing_model_checks) + ) + if missing_models: + typer.secho("Missing model downloads:", bold=True) + for check in missing_model_checks: + typer.echo( + f" {check.name}: " + f"{_format_bytes(check.download_size_bytes or 0)}" + ) + required_bytes = sum( + check.download_size_bytes or 0 + for check in missing_model_checks + ) + typer.secho( + "Maximum additional download and cache space: " + f"{_format_bytes(required_bytes)}", + bold=True, + ) + typer.echo(f"Model cache: {state.service.model_cache}") + free_bytes = available_storage_bytes(state.service.model_cache) + if free_bytes is not None: + typer.echo( + "Free space at model cache: " + f"{_format_bytes(free_bytes)}" + ) + command = "vidxp prepare --modalities " + ",".join(missing_models) + typer.secho( + "REMEDY: Download the selected model artifacts explicitly:", + fg=typer.colors.YELLOW, + ) + typer.secho(command, fg=typer.colors.YELLOW) + if not result.ok: raise typer.Exit(1) - if effective_output_format(state, json_output) == OutputFormat.rich: + if output_format == OutputFormat.rich: typer.secho( - "Selected VidXP dependencies are available.", + "Selected VidXP dependencies and model artifacts are available.", fg=typer.colors.GREEN, bold=True, ) @@ -71,13 +366,16 @@ def doctor( def prepare( ctx: typer.Context, modalities: Annotated[ - str, + list[str] | None, typer.Option( "--modalities", "-m", - help="Only prepare models for these modalities.", + help=( + "Only prepare models for these modalities. " + "Accepts comma-separated values or repeated options." + ), ), - ] = PREPARABLE_CAPABILITIES, + ] = None, capability_options: Annotated[ list[str] | None, typer.Option( @@ -92,27 +390,110 @@ def prepare( bool, typer.Option("--json", help="Emit machine-readable JSON."), ] = False, + detach: Annotated[ + bool, + typer.Option( + "--detach", + help="Return after the durable job is queued.", + ), + ] = False, + yes: Annotated[ + bool, + typer.Option( + "--yes", + "-y", + help="Confirm the displayed model download and cache size.", + ), + ] = False, ) -> None: """Download and cache selected runtime models before indexing.""" - selected = parse_modalities(modalities) state = state_from_context(ctx) - result = state.service.prepare_models( - selected, - capability_options=parse_capability_options(capability_options), - progress_callback=( - None - if state.quiet - or effective_output_format(state, json_output) - == OutputFormat.json - else lambda event: typer.echo(event["message"]) - ), + preparable = tuple( + capability.name + for capability in state.service.list_capabilities() + if capability.prepares_models + ) + selected = ( + preparable + if modalities is None + else parse_modalities(",".join(modalities), preparable) + ) + output_format = effective_output_format(state, json_output) + readiness = state.service.model_readiness(selected) + missing = tuple(check for check in readiness.checks if not check.ok) + required_bytes = sum( + check.download_size_bytes or 0 + for check in missing + ) + free_bytes = ( + available_storage_bytes(state.service.model_cache) + if missing + else None + ) + if missing and output_format == OutputFormat.rich: + typer.secho("Models to download:", bold=True) + for check in missing: + typer.echo( + f" {check.name}: " + f"{_format_bytes(check.download_size_bytes or 0)}" + ) + typer.secho( + "Maximum additional download and cache space: " + f"{_format_bytes(required_bytes)}", + bold=True, + ) + typer.echo(f"Model cache: {state.service.model_cache}") + if free_bytes is not None: + typer.echo( + "Free space at model cache: " + f"{_format_bytes(free_bytes)}" + ) + if ( + missing + and free_bytes is not None + and free_bytes < required_bytes + ): + raise typer.BadParameter( + "The model cache does not have enough free space for the " + f"selected downloads ({_format_bytes(required_bytes)} required, " + f"{_format_bytes(free_bytes)} free at " + f"{state.service.model_cache}). Choose another VidXP data " + "location with the global --data-dir option or VIDXP_DATA_DIR." + ) + if missing and not yes: + if output_format == OutputFormat.json: + raise typer.BadParameter( + "Model downloads require explicit confirmation; review " + "`vidxp doctor --modalities ... --json`, then rerun prepare " + "with --yes." + ) + typer.confirm("Download these models?", abort=True) + show_progress = not state.quiet and output_format == OutputFormat.rich + if show_progress: + emit_progress( + "Starting model preparation for " + ", ".join(selected) + "." + ) + job = state.jobs.submit_prepare_models( + PrepareModelsCommand( + modalities=selected, + capability_options=parse_capability_options(capability_options), + ) ) - if effective_output_format(state, json_output) == OutputFormat.json: - emit_json(result) + if not detach: + job = state.jobs.wait( + job.job_id, + progress=emit_job_progress if show_progress else None, + ) + if output_format == OutputFormat.json: + emit_json(job.model_dump(mode="json")) else: typer.secho( - "Selected VidXP runtime models are prepared.", + ( + f"Model preparation job queued: {job.job_id}" + if detach + else f"Selected runtime models are prepared: {job.job_id}" + ), fg=typer.colors.GREEN, bold=True, ) @@ -136,35 +517,45 @@ def ui( ) -> None: """Launch Streamlit with the selected repository configuration.""" + require_media_runtime() state = state_from_context(ctx) - os.environ["VIDXP_CONFIG_FILE"] = str(state.registry.path) - os.environ["VIDXP_REPOSITORY"] = state.repository.name - os.environ["VIDXP_INDEX_DIR"] = str(state.service.index_directory) - if state.service.device is None: - os.environ.pop("VIDXP_DEVICE", None) - else: - os.environ["VIDXP_DEVICE"] = state.service.device - + show_progress = ( + not state.quiet and state.output_format == OutputFormat.rich + ) + if show_progress: + emit_progress("Loading the browser interface...") try: from vidxp import frontend except ModuleNotFoundError as exc: if exc.name == "streamlit": - raise RuntimeError( - "The browser interface requires the frontend extra. " - "Install vidxp[frontend]." + raise ApplicationError( + "frontend_unavailable", + ErrorCategory.unavailable, + "The browser interface is unavailable. " + "In a source checkout, rerun uv sync with all profiles " + "you use and include --extra frontend. Published package: " + 'pip install "vidxp[frontend]".', ) from exc raise - frontend.SERVICE = state.service - frontend.SAVED_VIDEO_PATH = ( - state.service.index_directory / "source-video.mp4" - ) - frontend.ACTOR_OUTPUT_PATH = ( - state.service.index_directory / "actor-result.mp4" - ) streamlit_arguments = [] if host is not None: streamlit_arguments.append(f"--server.address={host}") if port is not None: streamlit_arguments.append(f"--server.port={port}") - frontend.main(streamlit_arguments) + if show_progress: + emit_progress("Starting the browser interface...") + try: + frontend.main( + streamlit_arguments, + settings=state.settings, + ) + finally: + try: + state.jobs.stop_worker() + except ApplicationError as exc: + typer.secho( + f"WARNING: The local background worker did not stop: {exc}", + fg=typer.colors.YELLOW, + err=True, + ) diff --git a/src/vidxp/cli_commands/search.py b/src/vidxp/cli_commands/search.py index d2c9f90..086dd46 100644 --- a/src/vidxp/cli_commands/search.py +++ b/src/vidxp/cli_commands/search.py @@ -1,72 +1,98 @@ from __future__ import annotations -from typing import Annotated, Callable +from typing import Annotated import typer -from vidxp.capabilities.registry import CAPABILITIES -from vidxp.capabilities.schemas import SearchResult +from vidxp.application_models import SearchCommand, SearchJobResult +from vidxp.application_models import FusedSearchResult from vidxp.cli_support import ( CLIState, + OutputFormat, effective_output_format, + emit_job_progress, + emit_progress, emit_search, state_from_context, ) -app = typer.Typer(no_args_is_help=True, help="Search the active index.") - - def run_search( state: CLIState, capability: str, query: str, *, + media_id: str | None, top_k: int, json_output: bool, -) -> SearchResult: - result = state.service.search(capability, query, top_k=top_k) +) -> FusedSearchResult: + output_format = effective_output_format(state, json_output) + show_progress = not state.quiet and output_format == OutputFormat.rich + if show_progress: + emit_progress(f"Starting {capability} search...") + job = state.jobs.submit_search( + SearchCommand( + modalities=(capability,), + query=query, + media_id=media_id, + top_k=top_k, + ) + ) + completed = state.jobs.wait( + job.job_id, + progress=emit_job_progress if show_progress else None, + ) + if not isinstance(completed.result, SearchJobResult): + raise RuntimeError("The completed search job has no search result.") + result = completed.result.result emit_search( result, - output_format=effective_output_format(state, json_output), + output_format=output_format, ) return result -def _search_command(capability: str) -> Callable: - def command( - ctx: typer.Context, - query: Annotated[ - str, - typer.Argument(help="Text query to find."), - ], - top_k: Annotated[ - int, - typer.Option( - "--top-k", - "-k", - min=1, - help="Maximum ranked hits.", +def search( + ctx: typer.Context, + capability: Annotated[ + str, + typer.Argument(help="Registered capability to query."), + ], + query: Annotated[ + str, + typer.Argument(help="Text query to find."), + ], + media_id: Annotated[ + str | None, + typer.Option( + "--media-id", + help=( + "Search only this media ID. Omit to rank matches across every " + "media item in the active index snapshot." ), - ] = 10, - json_output: Annotated[ - bool, - typer.Option("--json", help="Emit machine-readable JSON."), - ] = False, - ) -> None: - run_search( - state_from_context(ctx), - capability, - query, - top_k=top_k, - json_output=json_output, - ) - - command.__name__ = f"search_{capability}" - command.__doc__ = CAPABILITIES[capability].description - return command - - -for _name, _capability in CAPABILITIES.items(): - if "search" in _capability.operations: - app.command(_name)(_search_command(_name)) + ), + ] = None, + top_k: Annotated[ + int, + typer.Option( + "--top-k", + "-k", + min=1, + max=100, + help="Maximum ranked hits.", + ), + ] = 10, + json_output: Annotated[ + bool, + typer.Option("--json", help="Emit machine-readable JSON."), + ] = False, +) -> None: + state = state_from_context(ctx) + run_search( + state, + capability, + query, + media_id=media_id, + top_k=top_k, + json_output=json_output, + ) diff --git a/src/vidxp/cli_support.py b/src/vidxp/cli_support.py index 67947f9..934c3b5 100644 --- a/src/vidxp/cli_support.py +++ b/src/vidxp/cli_support.py @@ -2,6 +2,7 @@ import json from dataclasses import dataclass +from datetime import datetime from enum import Enum from typing import Any, Iterable @@ -16,13 +17,21 @@ ) from rich.table import Table -from vidxp.application import VidXPService -from vidxp.capabilities.registry import ( - index_capability_names, - validate_capability_names, +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + FusedSearchResult, + QueryAnswer, ) -from vidxp.capabilities.schemas import SearchResult +from vidxp.media_runtime import media_runtime_is_initialized from vidxp.repositories import RepositoryConfig, RepositoryRegistry +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from vidxp.application import VidXPApplication + from vidxp.composition import LocalApplicationContext + from vidxp.job_service import JobService + from vidxp.settings import VidXPSettings class OutputFormat(str, Enum): @@ -32,12 +41,24 @@ class OutputFormat(str, Enum): @dataclass class CLIState: - service: VidXPService + local: "LocalApplicationContext" registry: RepositoryRegistry repository: RepositoryConfig output_format: OutputFormat = OutputFormat.rich quiet: bool = False + @property + def service(self) -> "VidXPApplication": + return self.local.application + + @property + def jobs(self) -> "JobService": + return self.local.jobs + + @property + def settings(self) -> "VidXPSettings": + return self.local.settings + def state_from_context(ctx: typer.Context) -> CLIState: state = ctx.ensure_object(CLIState) @@ -46,6 +67,21 @@ def state_from_context(ctx: typer.Context) -> CLIState: return state +def require_media_runtime() -> None: + if media_runtime_is_initialized(): + return + raise ApplicationError( + "media_runtime_uninitialized", + ErrorCategory.unavailable, + "FFmpeg and ffprobe are not initialized for local media work. " + "Run `vidxp init`, then retry this command.", + details={ + "remediation": "vidxp init", + "install_hint": "Run vidxp init", + }, + ) + + def effective_output_format( state: CLIState, json_output: bool, @@ -64,34 +100,97 @@ def emit_json(payload: Any) -> None: ) +def emit_progress( + message: str, + *, + updated_at: datetime | None = None, + newline: bool = True, +) -> None: + timestamp = (updated_at or datetime.now().astimezone()).astimezone() + typer.secho( + f"[{timestamp:%H:%M:%S}] {message}", + fg=typer.colors.BLUE, + nl=newline, + ) + + +def emit_job_progress(job: Any) -> None: + if job.progress is not None: + message = job.progress.message + if ( + job.progress.stage == "downloading_model" + and job.progress.current is not None + and job.progress.total + ): + gib = 1024**3 + mib = 1024**2 + unit = gib if job.progress.total >= gib else mib + suffix = "GiB" if unit == gib else "MiB" + message += ( + f" {job.progress.current / unit:.1f} of " + f"{job.progress.total / unit:.1f} {suffix}" + ) + emit_progress( + message, + updated_at=job.progress.updated_at, + ) + + def emit_search( - result: SearchResult, + result: FusedSearchResult, *, output_format: OutputFormat, ) -> None: if output_format == OutputFormat.json: - emit_json(result.to_dict()) + emit_json(result.model_dump(mode="json")) return - if not result.hits: - typer.echo(f"No {result.modality} matches found.") + if not result.moments: + typer.echo("No matching moments found.") return - table = Table(title=f"{result.modality.title()} search results") + table = Table(title="Fused search results") table.add_column("Rank", justify="right") table.add_column("Start", justify="right") table.add_column("End", justify="right") table.add_column("Score", justify="right") table.add_column("Video") - for hit in result.hits: + table.add_column("Modalities") + for moment in result.moments: table.add_row( - str(hit.rank), - f"{hit.start:.3f}s", - f"{hit.end:.3f}s", - f"{hit.score:.6f}", - hit.video_id, + str(moment.rank), + f"{moment.start:.3f}s", + f"{moment.end:.3f}s", + f"{moment.score:.6f}", + moment.media_id, + ", ".join(moment.modalities), ) Console().print(table) +def emit_query( + result: QueryAnswer, + *, + output_format: OutputFormat, +) -> None: + if output_format == OutputFormat.json: + emit_json(result.model_dump(mode="json")) + return + console = Console() + console.print(f"Mode: {result.mode.value}") + if result.claims: + for claim in result.claims: + console.print(f"• {claim.text}") + console.print(f" Evidence: {', '.join(claim.evidence_ids)}") + elif result.evidence: + console.print( + f"No generated answer; returning {len(result.evidence)} " + "evidence item(s)." + ) + else: + console.print("No supporting evidence found.") + if result.fallback_reason: + console.print(f"Fallback: {result.fallback_reason}") + + def emit_status( status: dict[str, Any], *, @@ -107,15 +206,8 @@ def emit_status( table.add_row("Message", str(status.get("message", "—"))) if updated_at := status.get("updated_at"): table.add_row("Updated", str(updated_at)) - if video := status.get("video"): - table.add_row( - "Video", - str(video.get("source_name") or video.get("path") or "—"), - ) summary = status.get("summary") or {} - if modalities := (summary.get("configuration") or {}).get( - "enabled_modalities" - ): + if modalities := summary.get("modalities"): table.add_row("Modalities", ", ".join(map(str, modalities))) Console().print(table) @@ -166,25 +258,30 @@ def update(self, event: dict[str, Any]) -> None: def selected_modalities( values: Iterable[str] | None, + available: Iterable[str], ) -> tuple[str, ...]: + supported = tuple(available) if values is None: - return index_capability_names() - try: - return validate_capability_names(values) - except ValueError as exc: - raise typer.BadParameter(str(exc)) from exc + return supported + selected = tuple(dict.fromkeys(values)) + unknown = sorted(set(selected) - set(supported)) + if unknown: + raise typer.BadParameter( + "Unknown or unsupported capabilities: " + ", ".join(unknown) + ) + return selected -def parse_modalities(value: str) -> tuple[str, ...]: +def parse_modalities( + value: str, + available: Iterable[str], +) -> tuple[str, ...]: selected = tuple( item.strip().lower() for item in value.split(",") if item.strip() ) - try: - return validate_capability_names(selected) - except ValueError as exc: - raise typer.BadParameter(str(exc)) from exc + return selected_modalities(selected, available) def parse_capability_options( diff --git a/src/vidxp/database_cli.py b/src/vidxp/database_cli.py new file mode 100644 index 0000000..147c81f --- /dev/null +++ b/src/vidxp/database_cli.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import argparse +from importlib.resources import as_file, files +from typing import Sequence + +from alembic import command +from alembic.config import Config + +from vidxp.settings import ApplicationMode, VidXPSettings +from vidxp.workflow_runtime import workflow_database_url + + +def upgrade_database(database_url: str) -> None: + migration_root = files("vidxp.migrations") + with as_file(migration_root) as path: + config = Config() + config.set_main_option("script_location", str(path)) + config.set_main_option( + "sqlalchemy.url", + database_url.replace("%", "%%"), + ) + command.upgrade(config, "head") + + +def main(arguments: Sequence[str] | None = None) -> None: + parser = argparse.ArgumentParser( + description="Apply VidXP relational database migrations." + ) + parser.parse_args(arguments) + settings = VidXPSettings( + mode=ApplicationMode.server, + runtime_backend="cpu", + ) + upgrade_database(workflow_database_url(settings)) + + +if __name__ == "__main__": + main() diff --git a/src/vidxp/entrypoint.py b/src/vidxp/entrypoint.py new file mode 100644 index 0000000..8d8b3c1 --- /dev/null +++ b/src/vidxp/entrypoint.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import os +import sys +from datetime import datetime + + +def _wants_json(arguments: list[str]) -> bool: + if "--json" in arguments: + return True + for index, value in enumerate(arguments): + if value == "--format" and index + 1 < len(arguments): + return arguments[index + 1].lower() == "json" + if value.startswith("--format="): + return value.split("=", 1)[1].lower() == "json" + return os.environ.get("VIDXP_OUTPUT_FORMAT", "").lower() == "json" + + +def startup_command(arguments: list[str]) -> str | None: + if ( + _wants_json(arguments) + or any( + value in arguments + for value in ("--quiet", "-q", "--help", "--version", "-V") + ) + ): + return None + root_values = { + "--repository", + "-r", + "--config", + "--index-dir", + "--data-dir", + "--device", + "--format", + } + command = [] + skip_next = False + for value in arguments: + if skip_next: + skip_next = False + continue + if not command and value in root_values: + skip_next = True + continue + if not command and value.startswith("-"): + continue + command.append(value) + if len(command) == 2: + break + path = tuple(command) + if path and path[0] in { + "benchmark", + "doctor", + "init", + "prepare", + "query", + "search", + "ui", + }: + return path[0] + if path in { + ("actors", "render"), + ("artifacts", "snippet"), + ("index", "create"), + ("media", "import"), + }: + return " ".join(path) + return None + + +def main() -> None: + command = startup_command(sys.argv[1:]) + if command is not None: + timestamp = datetime.now().astimezone() + print( + f"[{timestamp:%H:%M:%S}] Starting VidXP {command}...", + file=sys.stderr, + flush=True, + ) + from vidxp.cli import main as cli_main + + cli_main() diff --git a/src/vidxp/frontend.py b/src/vidxp/frontend.py index 84a26e2..ce15d84 100644 --- a/src/vidxp/frontend.py +++ b/src/vidxp/frontend.py @@ -1,50 +1,148 @@ -import hashlib +import argparse +import logging +import shutil import sys +import tempfile +from functools import lru_cache from pathlib import Path from typing import Sequence import streamlit as st -from vidxp.application import VidXPService -from vidxp.capabilities.registry import index_capability_names -from vidxp.index_state import IndexNotReadyError -from vidxp.index_worker import ( - cancel_indexing, - indexing_in_progress, - start_indexing, +from vidxp.app_paths import available_storage_bytes +from vidxp.application import VidXPApplication +from vidxp.application_models import ( + ApplicationError, + CreateActorOverlayCommand, + CreateIndexCommand, + DependencyCheckCommand, + ErrorCategory, + ImportMediaCommand, + JobState, + ListMediaCommand, + PrepareModelsCommand, + QueryVideoCommand, + SearchCommand, ) -from vidxp.repositories import resolve_repository +from vidxp.branding import PROJECT_URL, icon_path +from vidxp.composition import create_application, create_job_service +from vidxp.index_state import IndexNotReadyError +from vidxp.job_service import JobService +from vidxp.settings import LocalExecutionSettings, VidXPSettings + + +LOGGER = logging.getLogger(__name__) + +@lru_cache(maxsize=1) +def _configured_service( + settings: VidXPSettings | None = None, +) -> VidXPApplication: + return create_application(settings or _settings_from_arguments()) -def _configured_service() -> VidXPService: - _, repository = resolve_repository() - return VidXPService( - repository.index_directory, - device=repository.device, + +@lru_cache(maxsize=1) +def _configured_jobs(settings: VidXPSettings | None = None) -> JobService: + return create_job_service(settings or _settings_from_arguments()) + + +def _settings_from_arguments( + arguments: Sequence[str] | None = None, +) -> VidXPSettings: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--vidxp-settings-json") + parsed, _ = parser.parse_known_args( + list(sys.argv[1:] if arguments is None else arguments) ) + if parsed.vidxp_settings_json is None: + return VidXPSettings() + return LocalExecutionSettings.model_validate_json( + parsed.vidxp_settings_json + ).application_settings() -SERVICE = _configured_service() -SAVED_VIDEO_PATH = SERVICE.index_directory / "source-video.mp4" -ACTOR_OUTPUT_PATH = SERVICE.index_directory / "actor-result.mp4" INDEX_REQUESTED_KEY = "_vidxp_index_requested" INDEX_ERROR_KEY = "_vidxp_index_error" +INDEX_JOB_ID_KEY = "_vidxp_index_job_id" +PREPARE_JOB_ID_KEY = "_vidxp_prepare_job_id" +PREPARE_CANCEL_REQUESTED_KEY = "_vidxp_prepare_cancel_requested" SEARCH_RESULT_KEY = "_vidxp_search_result" CANCEL_REQUESTED_KEY = "_vidxp_cancel_requested" +MEDIA_ID_KEY = "_vidxp_media_id" +UPLOAD_TOKEN_KEY = "_vidxp_upload_token" +MEDIA_NOTICE_KEY = "_vidxp_media_notice" +INDEX_JOB_QUERY_PARAM = "index_job" +PREPARE_JOB_QUERY_PARAM = "prepare_job" +SEARCH_JOB_QUERY_PARAM = "search_job" +SEARCH_TYPE_QUERY_PARAM = "search_type" +SCENE_SAMPLE_FPS_DEFAULT = 1.0 +SCENE_DETAIL_PRESETS = (0.5, SCENE_SAMPLE_FPS_DEFAULT, 2.0) +SCENE_DETAIL_LABELS = { + 0.5: "Faster — every 2 seconds", + 1.0: "Balanced — every second", + 2.0: "Detailed — twice per second", +} + + +def _format_bytes(size: int) -> str: + gib = 1024**3 + mib = 1024**2 + if size >= gib: + return f"{size / gib:.2f} GiB" + return f"{size / mib:.1f} MiB" -def _video_hash(uploaded_video) -> str | None: +def _remember_job( + *, + job_id: str, + query_param: str, + search_type: str | None = None, +) -> None: + st.query_params[query_param] = job_id + if search_type is not None: + st.query_params[SEARCH_TYPE_QUERY_PARAM] = search_type + + +def _forget_job(query_param: str) -> None: + st.query_params.pop(query_param, None) + if query_param == SEARCH_JOB_QUERY_PARAM: + st.query_params.pop(SEARCH_TYPE_QUERY_PARAM, None) + + +def _restore_durable_jobs() -> None: + if INDEX_JOB_ID_KEY not in st.session_state: + if job_id := st.query_params.get(INDEX_JOB_QUERY_PARAM): + st.session_state[INDEX_JOB_ID_KEY] = str(job_id) + if PREPARE_JOB_ID_KEY not in st.session_state: + if job_id := st.query_params.get(PREPARE_JOB_QUERY_PARAM): + st.session_state[PREPARE_JOB_ID_KEY] = str(job_id) + if SEARCH_RESULT_KEY not in st.session_state: + if job_id := st.query_params.get(SEARCH_JOB_QUERY_PARAM): + st.session_state[SEARCH_RESULT_KEY] = { + "type": str( + st.query_params.get(SEARCH_TYPE_QUERY_PARAM) or "search" + ), + "query": "", + "job_id": str(job_id), + } + + +def _upload_token(uploaded_video): if uploaded_video is None: return None - return hashlib.sha256(uploaded_video.getvalue()).hexdigest() + return ( + getattr(uploaded_video, "file_id", None), + uploaded_video.name, + getattr(uploaded_video, "size", None), + ) -def _is_search_ready(status, uploaded_video) -> bool: +def _is_search_ready(status, media_id: str | None) -> bool: if not status or status.get("state") != "ready": return False - if uploaded_video is None: - return SAVED_VIDEO_PATH.is_file() - return status.get("video", {}).get("sha256") == _video_hash(uploaded_video) + return media_id is not None and media_id in ( + (status.get("summary") or {}).get("media_ids") or () + ) def _render_summary(summary): @@ -53,10 +151,9 @@ def _render_summary(summary): st.caption( " · ".join( ( - f"Language: {summary.get('language', '—')}", - f"Dialogue phrases: {summary.get('dialogue_phrases', 0):,}", - f"Scene frames: {summary.get('scene_frames', 0):,}", - f"Actor clusters: {summary.get('actor_clusters', 0):,}", + f"Media: {summary.get('media_count', 0):,}", + "Capabilities: " + + ", ".join(summary.get("modalities", ())), ) ) ) @@ -66,13 +163,34 @@ def _render_progress(event): st.markdown(f"⏳ {event['message']}") current, total = event.get("current"), event.get("total") if current is not None and total: + if event.get("stage") == "downloading_model": + text = f"{_format_bytes(current)} of {_format_bytes(total)}" + else: + text = f"{current:,} of {total:,}" st.progress( min(current / total, 1.0), - text=f"{current:,} of {total:,}", + text=text, + ) + + +def _get_job(jobs: JobService, job_id: str | None): + if job_id is None: + return None + try: + return jobs.get(job_id) + except ApplicationError as exc: + if exc.category == ErrorCategory.not_found: + LOGGER.info("Background job %s no longer exists.", job_id) + return None + LOGGER.warning( + "Could not query background job %s: %s", + job_id, + exc.code, ) + raise -def _render_index_status(status, active, uploaded_video, request_error=None): +def _render_index_status(status, active, media_id, request_error=None): if request_error: st.error(request_error) return @@ -85,9 +203,9 @@ def _render_index_status(status, active, uploaded_video, request_error=None): } _render_progress(event) elif not status or status.get("state") == "missing": - st.caption("First indexing may download missing runtime model weights.") + st.caption("Prepare the selected models, then start indexing.") elif status["state"] == "ready": - if _is_search_ready(status, uploaded_video): + if _is_search_ready(status, media_id): st.success(status.get("message", "The video index is ready.")) _render_summary(status.get("summary")) else: @@ -114,7 +232,9 @@ def _request_indexing(): def _request_cancellation(): - if cancel_indexing(): + job_id = st.session_state.get(INDEX_JOB_ID_KEY) + if job_id is not None: + _configured_jobs().cancel(job_id) st.session_state[CANCEL_REQUESTED_KEY] = True st.session_state.pop(INDEX_ERROR_KEY, None) else: @@ -123,83 +243,209 @@ def _request_cancellation(): ) +def _request_prepare_cancellation(): + job_id = st.session_state.get(PREPARE_JOB_ID_KEY) + if job_id is not None: + _configured_jobs().cancel(job_id) + st.session_state[PREPARE_CANCEL_REQUESTED_KEY] = True + + def _available_index_modalities() -> tuple[str, ...]: + service = _configured_service() + return tuple( + capability.name + for capability in service.list_capabilities() + if capability.supports_indexing + if service.check_dependencies( + DependencyCheckCommand( + modalities=(capability.name,), + include_runtime_checks=False, + ) + ).ok + ) + + +def _available_query_modalities( + configured: tuple[str, ...], +) -> tuple[str, ...]: + service = _configured_service() + + def supports_query(capability_name: str) -> bool: + capability = service.get_capability(capability_name) + operations = { + operation.name for operation in capability.operations + } + return ( + "search" in operations + or {"clusters", "detections"}.issubset(operations) + ) + return tuple( - name - for name in index_capability_names() - if SERVICE.check_dependencies((name,))["ok"] + capability.name + for capability in service.list_capabilities() + if capability.name in configured + if supports_query(capability.name) ) -def _run_indexing(uploaded_video, status, modalities): +def _finish_index_job(job) -> None: + _forget_job(INDEX_JOB_QUERY_PARAM) + if job.state == JobState.succeeded: + st.session_state.pop(INDEX_ERROR_KEY, None) + elif job.error is not None: + st.session_state[INDEX_ERROR_KEY] = job.error.message + elif job.state == JobState.cancelled: + st.session_state[INDEX_ERROR_KEY] = "Indexing was cancelled." + else: + st.session_state[INDEX_ERROR_KEY] = ( + "Indexing did not complete successfully." + ) + st.session_state.pop(INDEX_JOB_ID_KEY, None) + + +def _run_indexing( + uploaded_video, + status, + modalities, + *, + scene_sample_fps: float | None = None, +): + service = _configured_service() + temporary_path = None try: if uploaded_video is not None: - SAVED_VIDEO_PATH.parent.mkdir(parents=True, exist_ok=True) - SAVED_VIDEO_PATH.write_bytes(uploaded_video.getvalue()) - source_name = uploaded_video.name + suffix = Path(uploaded_video.name).suffix + with tempfile.NamedTemporaryFile( + mode="w+b", + suffix=suffix, + delete=False, + ) as temporary: + temporary_path = Path(temporary.name) + uploaded_video.seek(0) + shutil.copyfileobj(uploaded_video, temporary) + asset = service.import_media( + ImportMediaCommand( + path=temporary_path, + original_filename=Path(uploaded_video.name).name, + declared_mime_type=getattr(uploaded_video, "type", None), + ) + ) + media_id = asset.media_id + st.session_state[MEDIA_ID_KEY] = media_id + st.session_state[UPLOAD_TOKEN_KEY] = _upload_token(uploaded_video) else: - source_name = ( - status.get("video", {}).get("source_name", SAVED_VIDEO_PATH.name) - if status - else SAVED_VIDEO_PATH.name + media_id = st.session_state.get(MEDIA_ID_KEY) + if media_id is None and status: + media_ids = (status.get("summary") or {}).get("media_ids") or () + media_id = media_ids[0] if len(media_ids) == 1 else None + if media_id is None: + raise ValueError("Select or import media before indexing.") + service.require_models(modalities) + job = _configured_jobs().submit_index( + CreateIndexCommand( + media_id=media_id, + modalities=modalities, + scene_sample_fps=scene_sample_fps, ) - start_indexing( - str(SAVED_VIDEO_PATH), - source_name, - SERVICE, - modalities=modalities, - ) - except Exception as exc: - st.session_state[INDEX_ERROR_KEY] = f"{type(exc).__name__}: {exc}" + ) + st.session_state[INDEX_JOB_ID_KEY] = job.job_id + _remember_job( + job_id=job.job_id, + query_param=INDEX_JOB_QUERY_PARAM, + ) + except ApplicationError as exc: + st.session_state[INDEX_ERROR_KEY] = str(exc) + except Exception: + LOGGER.exception("Unexpected indexing request failure") + st.session_state[INDEX_ERROR_KEY] = ( + "Indexing could not be started. Check the application logs." + ) else: st.session_state.pop(INDEX_ERROR_KEY, None) finally: + if temporary_path is not None: + temporary_path.unlink(missing_ok=True) st.session_state[INDEX_REQUESTED_KEY] = False st.rerun() -def _run_search(search_type, query): +def _scene_sample_fps_control( + modalities: tuple[str, ...], + *, + disabled: bool, +) -> float | None: + if "scene" not in modalities: + return None + return float( + st.selectbox( + "Scene detail", + SCENE_DETAIL_PRESETS, + index=1, + format_func=SCENE_DETAIL_LABELS.__getitem__, + disabled=disabled, + help=( + "More scene detail can improve coverage but takes longer " + "to index and uses more storage." + ), + ) + ) + + +def _run_search(search_type, query, media_id=None): + service = _configured_service() try: - status = SERVICE.index_status() + status = service.index_status().model_dump(mode="json") if status.get("state") != "ready": raise IndexNotReadyError("The video index is not ready.") if search_type == "actor": - ACTOR_OUTPUT_PATH.unlink(missing_ok=True) - SERVICE.render_actor( - query, - SAVED_VIDEO_PATH, - ACTOR_OUTPUT_PATH, + job = _configured_jobs().submit_actor_overlay( + CreateActorOverlayCommand(cluster_id=query) + ) + _remember_job( + job_id=job.job_id, + query_param=SEARCH_JOB_QUERY_PARAM, + search_type=search_type, ) - if ( - not ACTOR_OUTPUT_PATH.is_file() - or ACTOR_OUTPUT_PATH.stat().st_size == 0 - ): - return {"error": "Actor result video could not be generated."} return { "type": search_type, "query": query, - "video_path": str(ACTOR_OUTPUT_PATH), + "job_id": job.job_id, } - result = SERVICE.search( - search_type, - query, - top_k=1, + if search_type == "natural-language": + job = _configured_jobs().submit_query( + QueryVideoCommand( + question=query, + media_id=media_id, + top_k=10, + ) + ) + else: + job = _configured_jobs().submit_search( + SearchCommand( + modalities=(search_type,), + query=query, + media_id=media_id, + top_k=1, + ) + ) + _remember_job( + job_id=job.job_id, + query_param=SEARCH_JOB_QUERY_PARAM, + search_type=search_type, ) - if not result.hits: - return {"error": f"No {search_type} match was found."} - hit = result.hits[0] return { "type": search_type, "query": query, - "timestamp": hit.start, - "hit": hit.to_dict(), - "video_path": str(SAVED_VIDEO_PATH), + "job_id": job.job_id, } except IndexNotReadyError as exc: return {"error": str(exc)} - except Exception as exc: - return {"error": f"{type(exc).__name__}: {exc}"} + except ApplicationError as exc: + return {"error": str(exc)} + except Exception: + LOGGER.exception("Unexpected search failure") + return {"error": "Search failed. Check the application logs."} def _render_search_result(result): @@ -209,49 +455,284 @@ def _render_search_result(result): st.error(error) return - video_path = Path(result["video_path"]) - if not video_path.is_file(): + if "job_id" in result: + + @st.fragment(run_every="1s") + def poll_search_job(): + label = ( + "actor overlay" + if result["type"] == "actor" + else "natural-language query" + if result["type"] == "natural-language" + else f"{result['type']} search" + ) + try: + job = _get_job(_configured_jobs(), result["job_id"]) + except ApplicationError as exc: + message = ( + f"The {label} status is temporarily unavailable. Retrying." + if exc.retryable + else f"The {label} status could not be read: {exc}" + ) + st.warning(message) + return + if job is None: + _forget_job(SEARCH_JOB_QUERY_PARAM) + st.session_state[SEARCH_RESULT_KEY] = { + "type": result["type"], + "query": result["query"], + "error": f"The {label} job is unavailable.", + } + st.rerun() + return + if job.state in {JobState.queued, JobState.running}: + if job.progress is not None: + _render_progress(job.progress.model_dump(mode="json")) + else: + action = ( + "Starting" + if job.state == JobState.queued + else "Running" + ) + st.markdown(f"⏳ {action} {label}...") + return + if job.state != JobState.succeeded or job.result is None: + _forget_job(SEARCH_JOB_QUERY_PARAM) + message = { + JobState.cancelled: f"The {label} was cancelled.", + }.get( + job.state, + ( + job.error.message + if job.error is not None + else f"The {label} did not complete successfully." + ), + ) + st.session_state[SEARCH_RESULT_KEY] = { + "type": result["type"], + "query": result["query"], + "error": message, + } + st.rerun() + return + completed = job.result.result + _forget_job(SEARCH_JOB_QUERY_PARAM) + if result["type"] == "actor": + resolved = { + "type": "actor", + "query": result["query"], + "artifact_id": completed.artifact_id, + } + elif result["type"] == "natural-language": + first_moment = ( + completed.moments[0] if completed.moments else None + ) + first_evidence = ( + completed.evidence[0] if completed.evidence else None + ) + resolved = { + "type": result["type"], + "query": completed.question, + "answer": completed.model_dump(mode="json"), + "media_id": ( + first_moment.media_id + if first_moment is not None + else getattr(first_evidence, "media_id", None) + ), + "timestamp": ( + first_moment.start + if first_moment is not None + else getattr(first_evidence, "start", 0) + ), + } + elif not completed.moments: + resolved = { + "type": result["type"], + "query": completed.query, + "error": f"No {result['type']} match was found.", + } + else: + moment = completed.moments[0] + resolved = { + "type": result["type"], + "query": completed.query, + "timestamp": moment.start, + "moment": moment.model_dump(mode="json"), + "media_id": moment.media_id, + } + st.session_state[SEARCH_RESULT_KEY] = resolved + st.rerun() + + poll_search_job() + return + + if result["type"] == "natural-language": + answer = result["answer"] + if answer["claims"]: + for claim in answer["claims"]: + st.markdown(f"- {claim['text']}") + st.caption( + "Evidence: " + ", ".join(claim["evidence_ids"]) + ) + elif answer["evidence"]: + st.info( + "The query returned evidence without generating unsupported " + "claims." + ) + else: + st.info("No supporting evidence was found.") + if answer.get("fallback_reason"): + st.caption(f"Fallback: {answer['fallback_reason']}") + if result.get("media_id") is None: + return + + service = _configured_service() + try: + resource = ( + service.open_artifact_content(result["artifact_id"]) + if result["type"] == "actor" + else service.open_media_content(result["media_id"]) + ) + except ApplicationError: st.error("The search result video is no longer available.") return search_type = result["type"] if search_type == "actor": - st.success(f"Actor cluster {result['query']}") - st.video(str(video_path), format="video/mp4", width="stretch") + st.success( + ( + f"Actor cluster {result['query']}" + if result.get("query") + else "Actor overlay" + ) + ) + st.video( + str(resource.path), + format=resource.mime_type, + width="stretch", + ) return timestamp = result["timestamp"] - st.success(f"Best {search_type} match: {timestamp:.3f} seconds") + result_label = ( + "Closest sampled scene" + if search_type == "scene" + else "Closest supporting evidence" + if search_type == "natural-language" + else f"Closest {search_type} match" + ) + st.success(f"{result_label}: {timestamp:.1f} seconds") + if search_type == "scene": + st.caption( + "Scene search ranks sampled frames by visual similarity. " + "It does not identify the first occurrence and is not reliable " + "for counting people." + ) st.video( - str(video_path), + str(resource.path), start_time=timestamp, width="stretch", ) -def _select_video(busy): +def _default_media_id(media_id, assets): + available_ids = {asset.media_id for asset in assets} + if media_id in available_ids: + return media_id + if len(assets) == 1: + return assets[0].media_id + return None + + +def _import_local_video(service, raw_path): + return service.import_media( + ImportMediaCommand(path=Path(raw_path.strip()).expanduser()) + ) + + +def _select_video(busy, media_id, media_page): + service = _configured_service() st.subheader("Video") - upload_slot = st.empty() - has_session_upload = st.session_state.get("video_upload") is not None - if busy and not has_session_upload and SAVED_VIDEO_PATH.is_file(): - uploaded_video = None - st.caption("Indexing the saved video.") + assets = tuple(media_page.items) if media_page is not None else () + media_id = _default_media_id(media_id, assets) + if media_id is not None: + st.session_state[MEDIA_ID_KEY] = media_id else: - with upload_slot: - uploaded_video = st.file_uploader( - "Upload an MP4, MOV, or AVI video", - type=["mp4", "mov", "avi"], - disabled=busy, - key="video_upload", + st.session_state.pop(MEDIA_ID_KEY, None) + + if assets: + asset_by_id = {asset.media_id: asset for asset in assets} + selected_media_id = st.selectbox( + "Registered video", + tuple(asset_by_id), + index=( + tuple(asset_by_id).index(media_id) + if media_id is not None + else 0 + ), + format_func=lambda value: ( + f"{asset_by_id[value].original_filename} " + f"({asset_by_id[value].duration_seconds:.1f}s)" + ), + disabled=busy, + ) + if selected_media_id != media_id: + st.session_state.pop(SEARCH_RESULT_KEY, None) + media_id = selected_media_id + st.session_state[MEDIA_ID_KEY] = media_id + if media_page.next_cursor is not None: + st.caption( + "Showing the first 100 registered videos. " + "Use the CLI or API to inspect the full catalog." ) + with st.expander("Import a large local video"): + st.caption( + "Use a local path to avoid sending large files through the " + "browser uploader." + ) + local_path = st.text_input( + "Local video path", + disabled=busy, + placeholder="C:\\Videos\\example.mp4 or /Users/me/Videos/example.mp4", + ) + if st.button( + "Register local video", + disabled=busy or not local_path.strip(), + ): + try: + imported = _import_local_video(service, local_path) + except ApplicationError as exc: + st.error(str(exc)) + else: + st.session_state[MEDIA_ID_KEY] = imported.media_id + st.session_state.pop(SEARCH_RESULT_KEY, None) + st.session_state[MEDIA_NOTICE_KEY] = ( + f"Registered {imported.original_filename}." + ) + st.rerun() + + uploaded_video = st.file_uploader( + "Upload an MP4, MOV, or AVI video", + type=["mp4", "mov", "avi"], + disabled=busy, + key="video_upload", + ) + if busy and uploaded_video is None and media_id is not None: + st.caption("Indexing the registered video.") + if uploaded_video is not None: st.video(uploaded_video, width=560) - elif SAVED_VIDEO_PATH.is_file(): - if not busy: - st.caption("Using the saved video.") - st.video(str(SAVED_VIDEO_PATH), width=560) - return uploaded_video + elif media_id is not None: + try: + resource = service.open_media_content(media_id) + except ApplicationError: + st.warning("The registered video is no longer available.") + else: + if not busy: + st.caption("Using the registered video.") + st.video(str(resource.path), width=560) + return uploaded_video, media_id def _search_controls(ready, uploaded_video, available_modalities): @@ -264,43 +745,159 @@ def _search_controls(ready, uploaded_video, available_modalities): ) st.caption(message) - type_column, query_column = st.columns( - [0.35, 0.65], - gap="small", - vertical_alignment="bottom", - ) - with type_column: - search_type = st.selectbox( - "Search type", - list(available_modalities), - disabled=not ready, + with st.form( + "video_search", + clear_on_submit=False, + enter_to_submit=True, + border=False, + ): + type_column, query_column = st.columns( + [0.35, 0.65], + gap="small", + vertical_alignment="bottom", ) - with query_column: - query = st.text_input( - "Actor cluster ID" if search_type == "actor" else "Search query", - placeholder=( - "For example: 1" - if search_type == "actor" - else "For example: Chef makes pizza and cuts it up." - ), + with type_column: + search_type = st.selectbox( + "Search type", + ["natural-language", *available_modalities], + disabled=not ready, + ) + with query_column: + query = st.text_input( + ( + "Actor cluster ID" + if search_type == "actor" + else "Question" + if search_type == "natural-language" + else "Search query" + ), + placeholder=( + "For example: 1" + if search_type == "actor" + else "For example: What happens after the taxi arrives?" + if search_type == "natural-language" + else "For example: Chef makes pizza and cuts it up." + ), + disabled=not ready, + key="video_search_query", + ) + clicked = st.form_submit_button( + "Search", disabled=not ready, ) - clicked = st.button("Search", disabled=not ready or not query.strip()) + if clicked and not query.strip(): + st.warning("Enter a search query.") + clicked = False return clicked, search_type, query def run(): - st.set_page_config(page_title="VidXP", page_icon="🎬", layout="wide") + application_icon = icon_path() + st.set_page_config( + page_title="VidXP", + page_icon=application_icon, + layout="wide", + ) + st.logo(application_icon, size="large", link=PROJECT_URL) + service = _configured_service() st.title("VidXP") st.caption("Index and search video by dialogue, scene, and actor.") - st.caption(f"Index repository: {SERVICE.index_directory}") + st.caption(f"Index repository: {service.layout.root}") + if notice := st.session_state.pop(MEDIA_NOTICE_KEY, None): + st.success(notice) - active = indexing_in_progress(SERVICE) + jobs = _configured_jobs() + _restore_durable_jobs() + job_id = st.session_state.get(INDEX_JOB_ID_KEY) + job_lookup_error = None + try: + current_job = _get_job(jobs, job_id) + except ApplicationError as exc: + current_job = None + job_lookup_error = exc + if job_id is not None and current_job is None: + if job_lookup_error is None: + st.session_state.pop(INDEX_JOB_ID_KEY, None) + _forget_job(INDEX_JOB_QUERY_PARAM) + job_id = None + elif ( + current_job is not None + and current_job.state not in {JobState.queued, JobState.running} + ): + _finish_index_job(current_job) + job_id = None + active = ( + job_id is not None + and ( + job_lookup_error is not None + or ( + current_job is not None + and current_job.state in {JobState.queued, JobState.running} + ) + ) + ) + prepare_job_id = st.session_state.get(PREPARE_JOB_ID_KEY) + prepare_error = None + try: + prepare_job = _get_job(jobs, prepare_job_id) + except ApplicationError as exc: + prepare_job = None + prepare_error = exc + if prepare_job_id is not None and prepare_job is None: + if prepare_error is None: + st.session_state.pop(PREPARE_JOB_ID_KEY, None) + _forget_job(PREPARE_JOB_QUERY_PARAM) + prepare_job_id = None + elif ( + prepare_job is not None + and prepare_job.state not in {JobState.queued, JobState.running} + ): + st.session_state.pop(PREPARE_JOB_ID_KEY, None) + _forget_job(PREPARE_JOB_QUERY_PARAM) + prepare_job_id = None + if prepare_job.state == JobState.succeeded: + st.session_state[MEDIA_NOTICE_KEY] = ( + "Selected model artifacts are prepared." + ) + elif prepare_job.error is not None: + prepare_error = ApplicationError( + prepare_job.error.code, + prepare_job.error.category, + prepare_job.error.message, + details=prepare_job.error.details, + retryable=prepare_job.error.retryable, + ) + preparing = ( + prepare_job_id is not None + and ( + prepare_error is not None + or ( + prepare_job is not None + and prepare_job.state in {JobState.queued, JobState.running} + ) + ) + ) + if not preparing: + st.session_state.pop(PREPARE_CANCEL_REQUESTED_KEY, None) if not active: st.session_state.pop(CANCEL_REQUESTED_KEY, None) requested = st.session_state.get(INDEX_REQUESTED_KEY, False) - busy = active or requested - status = SERVICE.index_status() + busy = active or requested or preparing + status = service.index_status().model_dump(mode="json") + media_id = st.session_state.get(MEDIA_ID_KEY) + if media_id is None: + indexed_media = (status.get("summary") or {}).get("media_ids") or () + if len(indexed_media) == 1: + media_id = indexed_media[0] + st.session_state[MEDIA_ID_KEY] = media_id + try: + media_page = service.list_media(ListMediaCommand(page_size=100)) + except ApplicationError: + media_page = None + st.warning( + "Registered videos are temporarily unavailable. " + "The current index remains usable." + ) installed_modalities = _available_index_modalities() video_column, workflow_column = st.columns( [0.95, 1.05], @@ -309,7 +906,18 @@ def run(): ) with video_column: - uploaded_video = _select_video(busy) + uploaded_video, media_id = _select_video( + busy, + media_id, + media_page, + ) + selected_media_id = ( + media_id + if uploaded_video is None + or st.session_state.get(UPLOAD_TOKEN_KEY) + == _upload_token(uploaded_video) + else None + ) with workflow_column: st.subheader("Build index") @@ -322,21 +930,146 @@ def run(): help="Install another capability extra to make it available here.", ) ) + scene_sample_fps = _scene_sample_fps_control( + selected_modalities, + disabled=busy, + ) + model_readiness = ( + service.model_readiness(selected_modalities) + if selected_modalities + else None + ) + missing_checks = ( + tuple( + check + for check in model_readiness.checks + if not check.ok + ) + if model_readiness is not None + else () + ) + missing_models = tuple(check.name for check in missing_checks) + required_download_bytes = sum( + check.download_size_bytes or 0 + for check in missing_checks + ) + model_cache_free_bytes = ( + available_storage_bytes(service.model_cache) + if missing_checks + else None + ) + insufficient_model_space = ( + model_cache_free_bytes is not None + and model_cache_free_bytes < required_download_bytes + ) if not installed_modalities: st.warning( "No indexing capabilities are installed. " 'Install one, for example: pip install "vidxp[scene]"' ) + if missing_models and not preparing: + st.warning( + "The following model artifacts must be downloaded:" + ) + for check in missing_checks: + st.caption( + f"• {check.name} — " + f"{_format_bytes(check.download_size_bytes or 0)}" + ) + st.caption( + "Maximum additional download and cache space: " + f"{_format_bytes(required_download_bytes)}" + ) + st.caption(f"Model cache: {service.model_cache}") + if model_cache_free_bytes is not None: + st.caption( + "Free space at model cache: " + f"{_format_bytes(model_cache_free_bytes)}" + ) + if insufficient_model_space: + st.error( + "There is not enough free space for these model " + "downloads. Free space at the displayed location or " + "restart VidXP with a different global --data-dir." + ) + confirmed = st.checkbox( + "I want to download these models and use this cache space.", + key=( + "_vidxp_confirm_models_" + + "_".join(selected_modalities) + ), + disabled=insufficient_model_space, + ) + if st.button( + "Download models", + type="primary", + disabled=busy + or insufficient_model_space + or not confirmed, + ): + preparation = jobs.submit_prepare_models( + PrepareModelsCommand(modalities=selected_modalities) + ) + st.session_state[PREPARE_JOB_ID_KEY] = preparation.job_id + _remember_job( + job_id=preparation.job_id, + query_param=PREPARE_JOB_QUERY_PARAM, + ) + st.rerun() + if preparing: + st.button( + "Cancel model preparation", + on_click=_request_prepare_cancellation, + disabled=st.session_state.get( + PREPARE_CANCEL_REQUESTED_KEY, + False, + ), + ) + if st.session_state.get(PREPARE_CANCEL_REQUESTED_KEY, False): + st.caption("Cancellation requested.") + if preparing: + + @st.fragment(run_every="1s") + def poll_model_preparation(): + try: + latest = _get_job(jobs, prepare_job_id) + except ApplicationError as exc: + st.warning( + "Model preparation status is temporarily unavailable. " + f"Retrying: {exc}" + ) + return + if latest is None: + st.error("The model preparation job is unavailable.") + return + if latest.state not in {JobState.queued, JobState.running}: + st.rerun() + return + if latest.progress is not None: + _render_progress( + latest.progress.model_dump(mode="json") + ) + else: + st.markdown("⏳ Model preparation is queued.") + + poll_model_preparation() + elif prepare_error is not None: + st.error(f"Model preparation failed: {prepare_error}") st.button( "Index video", type="primary", disabled=busy or not selected_modalities - or (uploaded_video is None and not SAVED_VIDEO_PATH.is_file()), + or bool(missing_models) + or (uploaded_video is None and media_id is None), help=( "Indexing is already running." - if busy - else "Build or replace the index. First use may download model weights." + if active or requested + else "Model preparation is running." + if preparing + else "Prepare the selected models before indexing." + if missing_models + else "Build or replace the index." ), on_click=_request_indexing, ) @@ -358,38 +1091,67 @@ def run(): @st.fragment(run_every="1s") def poll_index_status(): - latest_active = indexing_in_progress(SERVICE) + try: + latest_job = _get_job(jobs, job_id) + except ApplicationError as exc: + message = ( + "The indexing job status is temporarily unavailable. " + "Retrying." + if exc.retryable + else f"The indexing job status could not be read: {exc}" + ) + st.warning(message) + return + if latest_job is None: + st.session_state.pop(INDEX_JOB_ID_KEY, None) + _forget_job(INDEX_JOB_QUERY_PARAM) + st.error("The background indexing job is unavailable.") + st.rerun() + return + latest_active = latest_job.state in { + JobState.queued, + JobState.running, + } + if not latest_active: + _finish_index_job(latest_job) + st.rerun() + return + latest_status = ( + latest_job.progress.model_dump(mode="json") + if latest_job.progress is not None + else { + "state": "indexing", + "stage": "queued", + "message": "Indexing is queued.", + } + ) _render_index_status( - SERVICE.index_status(), - latest_active, - uploaded_video, + latest_status, + True, + selected_media_id, st.session_state.get(INDEX_ERROR_KEY), ) - if not latest_active: - st.rerun() poll_index_status() else: _render_index_status( status, False, - uploaded_video, + selected_media_id, st.session_state.get(INDEX_ERROR_KEY), ) - ready = not busy and _is_search_ready(status, uploaded_video) + ready = not busy and _is_search_ready(status, selected_media_id) configured_modalities = ( - (status.get("summary", {}).get("configuration") or {}).get( - "enabled_modalities", - ("scene", "dialogue", "actor"), + (status.get("summary") or {}).get( + "modalities", + (), ) if ready - else ("scene", "dialogue", "actor") + else () ) - available_modalities = tuple( - modality - for modality in ("scene", "dialogue", "actor") - if modality in configured_modalities + available_modalities = _available_query_modalities( + tuple(configured_modalities) ) search_clicked, search_type, query = _search_controls( ready, @@ -397,21 +1159,41 @@ def poll_index_status(): available_modalities, ) if search_clicked: - st.session_state[SEARCH_RESULT_KEY] = _run_search(search_type, query) + st.session_state[SEARCH_RESULT_KEY] = _run_search( + search_type, + query, + selected_media_id, + ) _render_search_result(st.session_state.get(SEARCH_RESULT_KEY)) if requested: - _run_indexing(uploaded_video, status, selected_modalities) + _run_indexing( + uploaded_video, + status, + selected_modalities, + scene_sample_fps=scene_sample_fps, + ) -def main(arguments: Sequence[str] = ()): +def main( + arguments: Sequence[str] = (), + settings: VidXPSettings | None = None, +): from streamlit.web import cli as streamlit_cli + application_arguments = [] + if settings is not None: + application_arguments = [ + "--", + "--vidxp-settings-json", + LocalExecutionSettings.from_settings(settings).model_dump_json(), + ] sys.argv = [ "streamlit", "run", str(Path(__file__).resolve()), *arguments, + *application_arguments, ] raise SystemExit(streamlit_cli.main()) diff --git a/src/vidxp/health_cli.py b/src/vidxp/health_cli.py new file mode 100644 index 0000000..da6b269 --- /dev/null +++ b/src/vidxp/health_cli.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import argparse +from collections.abc import Sequence +from time import monotonic, sleep +from urllib.request import urlopen + +from sqlalchemy import create_engine, select + +from vidxp.core.storage import ( + BUNDLED_CHROMA_SERVER_URL, + ChromaClientFactory, +) +from vidxp.settings import VidXPSettings +from vidxp.workflow_runtime import workflow_database_url + + +def main(arguments: Sequence[str] | None = None) -> None: + parser = argparse.ArgumentParser(description="Check VidXP dependencies.") + parser.add_argument("role", choices=("chroma", "database", "worker")) + options = parser.parse_args(arguments) + settings = VidXPSettings() + if options.role == "chroma": + deadline = monotonic() + 120 + endpoint = BUNDLED_CHROMA_SERVER_URL + "/api/v2/heartbeat" + while True: + try: + with urlopen(endpoint, timeout=3) as response: + if response.status == 200: + return + except OSError: + if monotonic() >= deadline: + raise + sleep(1) + engine = create_engine( + workflow_database_url(settings), + pool_pre_ping=True, + ) + try: + with engine.connect() as connection: + connection.execute(select(1)).scalar_one() + finally: + engine.dispose() + if options.role == "worker": + ChromaClientFactory(BUNDLED_CHROMA_SERVER_URL).heartbeat() + + +if __name__ == "__main__": + main() diff --git a/src/vidxp/hook_app.py b/src/vidxp/hook_app.py new file mode 100644 index 0000000..fbb6b5a --- /dev/null +++ b/src/vidxp/hook_app.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import asyncio +import logging +from contextlib import asynccontextmanager, suppress +from typing import AsyncIterator + +from fastapi import FastAPI +from fastapi import HTTPException + +from vidxp.composition import UploadHookContext, create_upload_hook_context +from vidxp.infrastructure.tusd_contracts import ( + TusdHookRequest, + TusdHookResponse, +) +from vidxp.settings import VidXPSettings +from vidxp.tusd_hooks import TusdHookService + +LOGGER = logging.getLogger(__name__) + + +def create_hook_app( + settings: VidXPSettings | None = None, + *, + context: UploadHookContext | None = None, +) -> FastAPI: + active_context = context or create_upload_hook_context(settings) + owns_context = context is None + hooks = TusdHookService( + uploads=active_context.uploads, + authenticator=active_context.authenticator, + authorization=active_context.authorization, + ) + recovery_task: asyncio.Task[None] | None = None + + async def recover() -> None: + interval = active_context.settings.upload_recovery_interval_seconds + while True: + try: + await asyncio.to_thread(active_context.uploads.reconcile) + except Exception: + LOGGER.exception("The resumable-upload recovery sweep failed.") + await asyncio.sleep(interval) + + @asynccontextmanager + async def lifespan(_app: FastAPI) -> AsyncIterator[None]: + nonlocal recovery_task + recovery_task = asyncio.create_task(recover()) + try: + yield + finally: + recovery_task.cancel() + with suppress(asyncio.CancelledError): + await recovery_task + recovery_task = None + if owns_context: + active_context.close() + + app = FastAPI( + title="VidXP tusd hooks", + docs_url=None, + redoc_url=None, + openapi_url=None, + lifespan=lifespan, + ) + + @app.get("/health") + def health() -> dict[str, str]: + try: + if recovery_task is None or recovery_task.done(): + raise RuntimeError("The upload recovery task is not running.") + active_context.catalog.health() + except Exception as exc: + raise HTTPException( + status_code=503, + detail="The upload catalog is unavailable.", + ) from exc + return {"status": "ok"} + + @app.post("/hooks") + def handle(hook: TusdHookRequest) -> dict: + response: TusdHookResponse = hooks.handle(hook) + return response.wire() + + return app diff --git a/src/vidxp/hook_cli.py b/src/vidxp/hook_cli.py new file mode 100644 index 0000000..0041da2 --- /dev/null +++ b/src/vidxp/hook_cli.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import uvicorn + +from vidxp.hook_app import create_hook_app +from vidxp.settings import VidXPSettings + + +def main() -> None: + settings = VidXPSettings() + uvicorn.run( + create_hook_app(settings), + host=settings.http_bind_host, + port=settings.http_port, + access_log=False, + ) + + +if __name__ == "__main__": + main() diff --git a/src/vidxp/mcp.py b/src/vidxp/mcp.py new file mode 100644 index 0000000..e9a297f --- /dev/null +++ b/src/vidxp/mcp.py @@ -0,0 +1,823 @@ +from __future__ import annotations + +import logging +import json +from contextvars import ContextVar +from dataclasses import dataclass +from functools import partial +from typing import Annotated, Callable, TypeVar + +import anyio +from mcp.server import MCPServer +from mcp.server.auth.middleware.auth_context import get_access_token +from mcp.server.auth.provider import AccessToken +from mcp.server.auth.settings import AuthSettings +from mcp.server.transport_security import TransportSecuritySettings +from mcp.server.transport_security import TransportSecurityMiddleware +from mcp.shared.exceptions import MCPError +from mcp.server.mcpserver.exceptions import ToolError +from mcp.types import Icon, ResourceLink, ToolAnnotations +from pydantic import Field +from starlette.requests import Request +from starlette.types import ASGIApp, Receive, Scope, Send + +from vidxp import __version__ +from vidxp.application_models import ( + ApplicationError, + CapabilityInfo, + CapabilityList, + CreateSnippetCommand, + CreateIndexCommand, + ErrorCategory, + Identifier, + IndexStatus, + Job, + JobId, + JobPage, + ListJobsCommand, + ListMediaCommand, + MediaAsset, + MediaId, + MediaPage, + Principal, + PrepareModelsCommand, + QueryVideoCommand, + RuntimeReadiness, + SearchCommand, +) +from vidxp.authentication import ( + OIDCBearerAuthenticator, + create_authenticator, +) +from vidxp.authorization import RepositoryPermission +from vidxp.branding import ( + ICON_MIME_TYPE, + ICON_SIZE, + PROJECT_URL, + icon_data_uri, +) +from vidxp.composition import ControlPlaneContext, HttpApplicationContext +from vidxp.idempotency import IdempotencyKey, scoped_job_id +from vidxp.core.identifiers import ArtifactId +from vidxp.settings import HttpAuthMode + + +_LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T") +_REQUEST_PRINCIPAL: ContextVar[Principal | None] = ContextVar( + "vidxp_mcp_principal", + default=None, +) +_ERROR_CODES = { + ErrorCategory.validation: -32602, + ErrorCategory.authentication: -32001, + ErrorCategory.authorization: -32003, + ErrorCategory.not_found: -32004, + ErrorCategory.conflict: -32009, + ErrorCategory.resource_limit: -32029, + ErrorCategory.cancelled: -32040, + ErrorCategory.unavailable: -32050, + ErrorCategory.internal: -32603, +} +_READ_ONLY = ToolAnnotations( + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=False, +) +_SUBMIT = ToolAnnotations( + read_only_hint=False, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=False, +) +_CANCEL = ToolAnnotations( + read_only_hint=False, + destructive_hint=True, + idempotent_hint=True, + open_world_hint=False, +) + + +class PrincipalBridge: + """Carry the principal validated by the outer ASGI boundary into tools.""" + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + principal = scope.get("vidxp.principal") + reset = None + if isinstance(principal, Principal): + reset = _REQUEST_PRINCIPAL.set(principal) + try: + await self.app(scope, receive, send) + finally: + if reset is not None: + _REQUEST_PRINCIPAL.reset(reset) + + +class VidXPTokenVerifier: + """Adapt the shared OIDC validator to the SDK resource-server contract.""" + + def __init__(self, authenticator: OIDCBearerAuthenticator) -> None: + self.authenticator = authenticator + + async def verify_token(self, token: str) -> AccessToken | None: + try: + verified = await anyio.to_thread.run_sync( + self.authenticator.authenticate_bearer, + token, + ) + except ApplicationError: + return None + principal = verified.principal + return AccessToken( + token=token, + client_id=principal.client_id or principal.subject, + subject=principal.subject, + scopes=sorted(principal.scopes), + expires_at=verified.expires_at, + resource=verified.resource, + claims=verified.claims, + ) + + +@dataclass(frozen=True) +class RemoteMCP: + server: MCPServer + app: ASGIApp + owns_authentication: bool + transport_security: TransportSecuritySettings + + +def _principal(default: Principal | None) -> Principal: + current = _REQUEST_PRINCIPAL.get() + if current is not None: + return current + token = get_access_token() + if token is not None: + return Principal( + subject=token.subject or token.client_id, + client_id=token.client_id, + scopes=frozenset(token.scopes), + ) + if default is not None: + return default + raise MCPError( + -32001, + "Valid bearer authentication is required.", + { + "code": "authentication_required", + "category": "authentication", + "retryable": False, + }, + ) + + +def _application_error(exc: ApplicationError) -> ToolError: + detail = exc.detail + data = { + "code": detail.code, + "category": detail.category.value, + "retryable": detail.retryable, + } + if detail.correlation_id is not None: + data["correlation_id"] = detail.correlation_id + for key in ( + "capability", + "errors", + "install_hint", + "remediation", + "required_scope", + ): + if key in detail.details: + data[key] = detail.details[key] + return ToolError( + json.dumps( + { + "error": { + "protocol_code": _ERROR_CODES[detail.category], + "message": detail.message, + **data, + } + }, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ) + ) + + +def _invoke( + context: ControlPlaneContext, + *, + default_principal: Principal | None, + permission: RepositoryPermission, + operation: Callable[[Principal], _T], +) -> _T: + try: + principal = context.authorization.require( + _principal(default_principal), + permission, + ) + return operation(principal) + except MCPError: + raise + except ApplicationError as exc: + raise _application_error(exc) from exc + except Exception as exc: + _LOGGER.exception("Unexpected MCP tool failure.") + raise MCPError( + -32603, + "The MCP tool failed unexpectedly.", + { + "code": "internal_error", + "category": "internal", + "retryable": False, + }, + ) from exc + + +async def _invoke_async( + context: ControlPlaneContext, + *, + default_principal: Principal | None, + permission: RepositoryPermission, + operation: Callable[[Principal], _T], +) -> _T: + return await anyio.to_thread.run_sync( + partial( + _invoke, + context, + default_principal=default_principal, + permission=permission, + operation=operation, + ), + abandon_on_cancel=True, + ) + + +class MCPTransportSecurityBoundary: + """Run the SDK's transport checks before outer static authentication.""" + + def __init__( + self, + app: ASGIApp, + *, + settings: TransportSecuritySettings, + ) -> None: + self.app = app + self.security = TransportSecurityMiddleware(settings) + + async def __call__( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + if ( + scope["type"] == "http" + and str(scope.get("path", "")) in {"/mcp", "/mcp/"} + ): + response = await self.security.validate_request( + Request(scope, receive=receive), + is_post=str(scope.get("method", "")).upper() == "POST", + ) + if response is not None: + await response(scope, receive, send) + return + await self.app(scope, receive, send) + + +def create_mcp_server( + context: ControlPlaneContext, + *, + default_principal: Principal | None = None, + oidc_authentication: bool = False, +) -> MCPServer: + settings = context.settings + token_verifier = None + auth = None + if oidc_authentication: + assert settings.http_oidc_issuer is not None + assert settings.mcp_public_url is not None + if ( + isinstance(context, HttpApplicationContext) + and isinstance( + context.authenticator, + OIDCBearerAuthenticator, + ) + ): + authenticator = context.authenticator.for_audience( + settings.mcp_public_url, + ) + else: + authenticator = create_authenticator( + settings, + audience=settings.mcp_public_url, + required_scopes=(), + ) + if not isinstance(authenticator, OIDCBearerAuthenticator): + raise ValueError("OIDC MCP requires the OIDC authenticator.") + token_verifier = VidXPTokenVerifier(authenticator) + auth = AuthSettings( + issuer_url=settings.http_oidc_issuer, + resource_server_url=settings.mcp_public_url, + required_scopes=list(settings.http_required_scopes), + ) + + server = MCPServer( + name="vidxp", + title="VidXP", + description="Index and search registered video media.", + website_url=PROJECT_URL, + icons=[ + Icon( + src=icon_data_uri(), + mimeType=ICON_MIME_TYPE, + sizes=[ICON_SIZE], + ) + ], + instructions=( + "Call get_runtime_readiness before indexing. If selected model " + "artifacts are missing, submit prepare_models and poll get_job " + "until it completes. Discover registered media with list_media. " + "Register and upload new video through the HTTP/tus API, then use " + "its media_id with start_indexing. get_index_status identifies the " + "media included in the active index snapshot. For search_moments " + "and query_video, provide command.media_id to restrict work to one " + "video, or omit it to search/query across every media item in that " + "snapshot. To deliver a matching time range, submit create_clip, " + "poll get_job, then call get_artifact_download with the completed " + "job's artifact_id. Use list_jobs to recover job IDs across agent " + "sessions." + ), + version=__version__, + token_verifier=token_verifier, + auth=auth, + ) + + async def artifact_bytes( + artifact_id: ArtifactId, + *, + expected_mime_type: str, + ) -> bytes: + resource = await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: ( + context.application.open_artifact_content(artifact_id) + ), + ) + if resource.mime_type != expected_mime_type: + raise MCPError( + -32602, + "The artifact URI does not match its media type.", + { + "code": "artifact_media_type_mismatch", + "category": "validation", + "retryable": False, + }, + ) + return await anyio.to_thread.run_sync(resource.path.read_bytes) + + @server.resource( + "vidxp://artifacts/{artifact_id}/content.mp4", + name="vidxp_artifact_mp4", + title="VidXP MP4 artifact", + description=( + "Binary content for a generated VidXP clip or video artifact." + ), + mime_type="video/mp4", + ) + async def read_mp4_artifact(artifact_id: ArtifactId) -> bytes: + return await artifact_bytes( + artifact_id, + expected_mime_type="video/mp4", + ) + + @server.resource( + "vidxp://artifacts/{artifact_id}/content.mkv", + name="vidxp_artifact_matroska", + title="VidXP Matroska artifact", + description=( + "Binary content for a source-profile VidXP clip artifact." + ), + mime_type="video/x-matroska", + ) + async def read_matroska_artifact(artifact_id: ArtifactId) -> bytes: + return await artifact_bytes( + artifact_id, + expected_mime_type="video/x-matroska", + ) + + @server.tool( + description="List installed VidXP capabilities.", + annotations=_READ_ONLY, + structured_output=True, + ) + async def list_capabilities() -> CapabilityList: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: CapabilityList( + items=context.application.list_capabilities() + ), + ) + + @server.tool( + description="Get one capability and its public operation schemas.", + annotations=_READ_ONLY, + structured_output=True, + ) + async def get_capability(name: Identifier) -> CapabilityInfo: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: context.application.get_capability(name), + ) + + @server.tool( + description=( + "Check runtime components and whether model artifacts are prepared." + ), + annotations=_READ_ONLY, + structured_output=True, + ) + async def get_runtime_readiness() -> RuntimeReadiness: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: context.application.runtime_readiness(), + ) + + @server.tool( + description=( + "List registered video filenames, metadata, and stable media IDs " + "without transferring video bytes. Registration does not imply " + "that a video is present in the active index snapshot." + ), + annotations=_READ_ONLY, + structured_output=True, + ) + async def list_media( + page_size: Annotated[int, Field(gt=0, le=100)] = 50, + cursor: Annotated[ + str | None, + Field(min_length=1, max_length=512), + ] = None, + ) -> MediaPage: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: context.application.list_media( + ListMediaCommand(page_size=page_size, cursor=cursor) + ), + ) + + @server.tool( + description=( + "Get one registered video's metadata by the stable media ID " + "returned by list_media." + ), + annotations=_READ_ONLY, + structured_output=True, + ) + async def get_media(media_id: MediaId) -> MediaAsset: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: context.application.get_media(media_id), + ) + + @server.tool( + description=( + "Inspect the active index snapshot, including its indexed media " + "count and media IDs." + ), + annotations=_READ_ONLY, + structured_output=True, + ) + async def get_index_status() -> IndexStatus: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: context.application.index_status(), + ) + + @server.tool( + description=( + "Add or replace one registered media ID in the active multi-video " + "index snapshot. Obtain the ID from list_media or a completed " + "upload, then poll get_job." + ), + annotations=_SUBMIT, + structured_output=True, + ) + async def start_indexing( + command: CreateIndexCommand, + idempotency_key: IdempotencyKey, + ) -> Job: + def submit(actor: Principal) -> Job: + context.application.require_models(command.modalities) + return context.jobs.submit_index( + command, + job_id=scoped_job_id( + principal=actor, + transport="mcp", + operation="index", + idempotency_key=idempotency_key, + ), + ) + + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.write, + operation=submit, + ) + + @server.tool( + description=( + "Explicitly download and validate selected model artifacts. Poll " + "get_job for byte progress and completion before indexing." + ), + annotations=_SUBMIT, + structured_output=True, + ) + async def prepare_models( + command: PrepareModelsCommand, + idempotency_key: IdempotencyKey, + ) -> Job: + def submit(actor: Principal) -> Job: + return context.jobs.submit_prepare_models( + command, + job_id=scoped_job_id( + principal=actor, + transport="mcp", + operation="prepare-models", + idempotency_key=idempotency_key, + ), + ) + + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.write, + operation=submit, + ) + + @server.tool( + description=( + "Submit a durable ranked moment search. Set command.media_id to " + "search one registered video; omit it to search across every media " + "item in the active index snapshot. Poll get_job for top-k results." + ), + annotations=_SUBMIT, + structured_output=True, + ) + async def search_moments( + command: SearchCommand, + idempotency_key: IdempotencyKey, + ) -> Job: + def submit(actor: Principal) -> Job: + return context.jobs.submit_search( + command, + job_id=scoped_job_id( + principal=actor, + transport="mcp", + operation="search", + idempotency_key=idempotency_key, + ), + ) + + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=submit, + ) + + @server.tool( + description=( + "Submit a durable grounded natural-language query over indexed " + "moments and actor evidence. Set command.media_id for one video, " + "or omit it to query across every media item in the active index " + "snapshot. Poll get_job for the answer and evidence." + ), + annotations=_SUBMIT, + structured_output=True, + ) + async def query_video( + command: QueryVideoCommand, + idempotency_key: IdempotencyKey, + ) -> Job: + def submit(actor: Principal) -> Job: + return context.jobs.submit_query( + command, + job_id=scoped_job_id( + principal=actor, + transport="mcp", + operation="query", + idempotency_key=idempotency_key, + ), + ) + + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=submit, + ) + + @server.tool( + description=( + "Create a downloadable clip from a media ID and time range returned " + "by search_moments or query_video. Poll get_job, then pass the " + "completed result's artifact_id to get_artifact_download." + ), + annotations=_SUBMIT, + structured_output=True, + ) + async def create_clip( + command: CreateSnippetCommand, + idempotency_key: IdempotencyKey, + ) -> Job: + def submit(actor: Principal) -> Job: + return context.jobs.submit_snippet( + command, + job_id=scoped_job_id( + principal=actor, + transport="mcp", + operation="snippet", + idempotency_key=idempotency_key, + ), + ) + + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.write, + operation=submit, + ) + + @server.tool( + description=( + "Return a readable MCP resource link for a completed clip or video " + "artifact. The artifact_id is in the completed create_clip job " + "result; clients read the link only when they need the video bytes." + ), + annotations=_READ_ONLY, + structured_output=True, + ) + async def get_artifact_download( + artifact_id: ArtifactId, + ) -> ResourceLink: + artifact = await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: ( + context.application.get_artifact(artifact_id) + ), + ) + suffix = ( + "mp4" + if artifact.mime_type == "video/mp4" + else "mkv" + ) + filename = f"{artifact.kind.value}-{artifact.artifact_id}.{suffix}" + return ResourceLink( + name=filename, + title=f"VidXP {artifact.kind.value.replace('_', ' ')}", + uri=( + f"vidxp://artifacts/{artifact.artifact_id}/" + f"content.{suffix}" + ), + description=( + f"Generated from media {artifact.media_id}; " + f"{artifact.byte_size:,} bytes." + ), + mimeType=artifact.mime_type, + size=artifact.byte_size, + ) + + @server.tool( + description="List durable jobs so work can be recovered across sessions.", + annotations=_READ_ONLY, + structured_output=True, + ) + async def list_jobs( + page_size: Annotated[int, Field(gt=0, le=100)] = 50, + cursor: Annotated[ + str | None, + Field(min_length=1, max_length=512), + ] = None, + ) -> JobPage: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: context.jobs.list( + ListJobsCommand(page_size=page_size, cursor=cursor) + ), + ) + + @server.tool( + description="Poll a durable VidXP job and its typed result.", + annotations=_READ_ONLY, + structured_output=True, + ) + async def get_job(job_id: JobId) -> Job: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: context.jobs.get(job_id), + ) + + @server.tool( + description="Retry a failed or cancelled durable job.", + annotations=_SUBMIT, + structured_output=True, + ) + async def retry_job( + job_id: JobId, + idempotency_key: IdempotencyKey, + ) -> Job: + def retry(actor: Principal) -> Job: + return context.jobs.retry( + job_id, + retry_id=scoped_job_id( + principal=actor, + transport="mcp", + operation=f"retry:{job_id}", + idempotency_key=idempotency_key, + ), + ) + + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.write, + operation=retry, + ) + + @server.tool( + description="Explicitly cancel an active durable job.", + annotations=_CANCEL, + structured_output=True, + ) + async def cancel_job(job_id: JobId) -> Job: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.write, + operation=lambda _actor: context.jobs.cancel(job_id), + ) + + return server + + +def create_remote_mcp(context: ControlPlaneContext) -> RemoteMCP: + owns_authentication = ( + context.settings.http_auth_mode == HttpAuthMode.oidc + ) + server = create_mcp_server( + context, + oidc_authentication=owns_authentication, + ) + transport_security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=list(context.settings.mcp_allowed_hosts), + allowed_origins=list(context.settings.mcp_allowed_origins), + ) + app = server.streamable_http_app( + streamable_http_path="/mcp", + stateless_http=True, + json_response=False, + max_request_body_size=( + context.settings.mcp_max_request_body_bytes + ), + transport_security=transport_security, + host=context.settings.http_bind_host, + ) + return RemoteMCP( + server=server, + app=PrincipalBridge(app), + owns_authentication=owns_authentication, + transport_security=transport_security, + ) diff --git a/src/vidxp/mcp_cli.py b/src/vidxp/mcp_cli.py new file mode 100644 index 0000000..a4de11e --- /dev/null +++ b/src/vidxp/mcp_cli.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import shutil +import sys +from pathlib import Path +from typing import Any, Sequence + + +def mcp_executable() -> str: + """Return the most reliable executable path for desktop MCP clients.""" + + discovered = shutil.which("vidxp-mcp") + if discovered is not None: + return str(Path(discovered).resolve()) + executable_name = "vidxp-mcp.exe" if os.name == "nt" else "vidxp-mcp" + sibling = Path(sys.executable).with_name(executable_name) + if sibling.is_file(): + return str(sibling.resolve()) + return "vidxp-mcp" + + +def stdio_client_config( + *, + command: str | None = None, + registry: str | None = None, + repository: str | None = "default", + index_directory: str | None = None, + data_directory: Path | None = None, + device: str | None = None, +) -> dict[str, Any]: + """Build a broadly supported copy/paste MCP client configuration.""" + + arguments: list[str] = [] + for flag, value in ( + ("--registry", registry), + ("--repository", repository), + ("--index-directory", index_directory), + ("--data-dir", data_directory), + ("--device", device), + ): + if value is not None: + arguments.extend((flag, str(value))) + return { + "mcpServers": { + "vidxp": { + "command": command or mcp_executable(), + "args": arguments, + } + } + } + + +def render_stdio_client_config(**options: Any) -> str: + return json.dumps( + stdio_client_config(**options), + ensure_ascii=False, + indent=2, + ) + + +def _parser() -> argparse.ArgumentParser: + example = render_stdio_client_config() + return argparse.ArgumentParser( + description="Run the local VidXP MCP server over stdio.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "COPY/PASTE MCP CLIENT CONFIG\n" + "Import this JSON into any stdio MCP client:\n\n" + f"{example}\n\n" + "Run `vidxp-mcp --print-config` to print only the JSON. Add the " + "same repository/data/device options to either command when you " + "need non-default values." + ), + ) + + +async def _inspect_server(server: Any) -> dict[str, Any]: + from mcp.client import Client + + async with Client(server) as client: + discovered = await client.list_tools() + status = await client.call_tool("get_index_status", {}) + if status.is_error: + raise RuntimeError("The MCP index-status probe failed.") + server_info = client.server_info + return { + "server": server_info.title or server_info.name, + "version": server_info.version, + "tools": [tool.name for tool in discovered.tools], + "index_state": (status.structured_content or {}).get( + "state", + "unknown", + ), + } + + +def main(arguments: Sequence[str] | None = None) -> None: + parser = _parser() + parser.add_argument( + "--registry", + help="Path to the named-repository configuration file.", + ) + parser.add_argument( + "--repository", + help="Named repository to use; the implicit default is 'default'.", + ) + parser.add_argument( + "--index-directory", + help="Override the selected repository's index directory.", + ) + parser.add_argument( + "--data-dir", + type=Path, + help="Store VidXP models and the default repository here.", + ) + parser.add_argument( + "--device", + help="Override the selected repository runtime device.", + ) + actions = parser.add_mutually_exclusive_group() + actions.add_argument( + "--print-config", + action="store_true", + help="Print import-ready MCP client JSON and exit.", + ) + actions.add_argument( + "--check", + action="store_true", + help=( + "Perform a local MCP handshake and tool probe, print the resolved " + "runtime paths, and exit." + ), + ) + options = parser.parse_args(arguments) + if options.print_config: + print( + render_stdio_client_config( + registry=options.registry, + repository=options.repository or "default", + index_directory=options.index_directory, + data_directory=options.data_dir, + device=options.device, + ) + ) + return + + from vidxp.application_models import Principal + from vidxp.composition import ( + create_control_plane_application, + create_local_application, + ) + try: + from vidxp.mcp import create_mcp_server + except ModuleNotFoundError as exc: + if exc.name == "mcp": + parser.error( + 'MCP support is not installed. Install "vidxp[mcp]" in this ' + "same environment, then run the command again." + ) + raise + + local = create_local_application( + registry_path=options.registry, + repository_name=options.repository, + index_directory=options.index_directory, + data_directory=options.data_dir, + device=options.device, + ) + context = create_control_plane_application(local.settings) + try: + server = create_mcp_server( + context, + default_principal=Principal( + subject="local", + client_id="stdio", + scopes=frozenset({"*"}), + ), + ) + if options.check: + result = asyncio.run(_inspect_server(server)) + print(f"OK {result['server']} MCP {result['version']}") + print(f"Repository: {local.repository.name}") + print(f"Data: {context.settings.data_dir}") + print(f"Index: {context.settings.repository_root}") + print(f"Index state: {result['index_state']}") + print( + f"Tools: {len(result['tools'])} " + f"({', '.join(result['tools'])})" + ) + return + server.run("stdio") + finally: + context.close() + local.close() + + +if __name__ == "__main__": + main() diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..b862c1e --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,1092 @@ +import asyncio +import gc +import unittest +import warnings +from dataclasses import replace +from datetime import datetime, timezone +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import Mock +from unittest.mock import patch + +import httpx +import jwt +from cryptography.hazmat.primitives.asymmetric import rsa +from fastapi.testclient import TestClient + +from vidxp.api import create_app +from vidxp.api_routes.dependencies import scoped_job_id +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + ComponentReadiness, + Job, + JobKind, + JobQueue, + JobState, + MediaAsset, + Principal, + SearchCommand, + QueryVideoCommand, + UploadIntent, +) +from vidxp.composition import ( + HttpApplicationContext, + create_http_application, +) +from vidxp.control_plane import ControlPlaneApplication +from vidxp.authentication import create_authenticator +from vidxp.authorization import AuthorizationPolicy +from vidxp.core.media import MediaState, MediaStream +from vidxp.core.uploads import UploadState +from vidxp.job_service import JobService +from vidxp.ports import LocalFileResource +from vidxp.readiness_service import ReadinessService +from vidxp.settings import HttpAuthMode, VidXPSettings +from vidxp.upload_service import RemoteUploadService + + +MEDIA_ID = "123456781234423481234567890abcde" +JOB_ID = "223456781234423481234567890abcde" +IDEMPOTENCY_KEY = "323456781234423481234567890abcde" +ARTIFACT_ID = "423456781234423481234567890abcde" +TOKEN = "a" * 32 + + +def media_asset() -> MediaAsset: + return MediaAsset( + media_id=MEDIA_ID, + video_id=MEDIA_ID, + original_filename="video.mp4", + sha256="1" * 64, + byte_size=10, + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + state=MediaState.ready, + created_at=datetime.now(timezone.utc), + ) + + +def queued_job() -> Job: + return Job( + job_id=JOB_ID, + kind=JobKind.index, + state=JobState.queued, + queue=JobQueue.cpu, + ) + + +class ApiTests(unittest.TestCase): + def context( + self, + root: Path, + *, + auth: HttpAuthMode = HttpAuthMode.none, + upload_limit: int = 256 * 1024 * 1024, + json_limit: int = 4 * 1024 * 1024, + mcp_limit: int = 4 * 1024 * 1024, + allowed_origins: tuple[str, ...] = (), + remote_uploads: bool = False, + ) -> HttpApplicationContext: + settings = VidXPSettings( + repository_root=root, + runtime_backend="cpu", + http_auth_mode=auth, + http_static_bearer_token=TOKEN if auth == HttpAuthMode.static else None, + http_oidc_issuer=( + "https://issuer.example" + if auth == HttpAuthMode.oidc + else None + ), + http_oidc_audience=( + "https://api.example" + if auth == HttpAuthMode.oidc + else None + ), + http_oidc_jwks_url=( + "https://issuer.example/jwks" + if auth == HttpAuthMode.oidc + else None + ), + http_required_scopes=( + ("vidxp.read",) + if auth == HttpAuthMode.oidc + else () + ), + mcp_public_url=( + "https://api.example/mcp" + if auth == HttpAuthMode.oidc + else None + ), + http_max_small_upload_bytes=upload_limit, + http_max_json_body_bytes=json_limit, + mcp_max_request_body_bytes=mcp_limit, + http_allowed_origins=allowed_origins, + upload_public_endpoint=( + "http://localhost:8080/uploads/" + if remote_uploads + else None + ), + upload_internal_endpoint=( + "http://localhost:8080/uploads/" + if remote_uploads + else None + ), + upload_cleanup_token="c" * 32 if remote_uploads else None, + ) + application = Mock() + jobs = Mock(spec=JobService) + readiness = Mock() + readiness.ready.return_value = True + uploads = Mock(spec=RemoteUploadService) if remote_uploads else None + return HttpApplicationContext( + application=application, + jobs=jobs, + readiness=readiness, + authenticator=create_authenticator(settings), + authorization=AuthorizationPolicy(), + settings=settings, + uploads=uploads, + ) + + @staticmethod + def auth() -> dict[str, str]: + return {"Authorization": f"Bearer {TOKEN}"} + + def test_local_http_context_stops_worker_when_closed(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + + context.close() + + context.jobs.stop_worker.assert_called_once_with() + context.jobs.close.assert_called_once_with() + + def test_health_and_minimal_readiness_are_public(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.static, + ) + with TestClient(create_app(context=context)) as client: + health = client.get("/health") + ready = client.get("/ready") + + self.assertEqual(health.status_code, 200) + self.assertEqual(health.json(), {"status": "ok"}) + self.assertEqual(ready.status_code, 200) + self.assertEqual(ready.json(), {"ready": True, "status": "ready"}) + + def test_resumable_upload_api_uses_typed_intents_and_private_urls(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.static, + remote_uploads=True, + ) + assert context.uploads is not None + now = datetime.now(timezone.utc) + intent = UploadIntent( + intent_id="123456781234423481234567890abcde", + original_filename="video.mp4", + byte_size=20, + declared_mime_type="video/mp4", + state=UploadState.pending, + created_at=now, + expires_at=now.replace(year=now.year + 1), + ) + context.uploads.create_intent.return_value = intent + context.uploads.upload_url.return_value = None + with TestClient(create_app(context=context)) as client: + created = client.post( + "/api/v1/media/uploads", + headers={ + **self.auth(), + "Idempotency-Key": IDEMPOTENCY_KEY, + }, + json={ + "original_filename": "video.mp4", + "byte_size": 20, + "declared_mime_type": "video/mp4", + }, + ) + + self.assertEqual(created.status_code, 201) + self.assertEqual(created.headers["cache-control"], "private, no-store") + self.assertEqual(created.headers["referrer-policy"], "no-referrer") + self.assertEqual( + created.headers["location"], + f"/api/v1/media/uploads/{intent.intent_id}", + ) + payload = created.json() + self.assertEqual(payload["intent"]["state"], "pending") + self.assertEqual( + payload["creation_url"], + "http://localhost:8080/uploads/", + ) + self.assertIsNone(payload["resume_url"]) + self.assertTrue(payload["upload_metadata"].startswith("intent_id ")) + + def test_readiness_failure_returns_503_without_details(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.readiness.ready.return_value = False + with TestClient(create_app(context=context)) as client: + response = client.get("/ready") + + self.assertEqual(response.status_code, 503) + self.assertEqual( + response.json(), + {"ready": False, "status": "not_ready"}, + ) + + def test_static_bearer_is_enforced_before_dispatch(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.static, + ) + context.application.list_capabilities.return_value = () + with TestClient(create_app(context=context)) as client: + missing = client.get("/api/v1/capabilities") + accepted = client.get( + "/api/v1/capabilities", + headers=self.auth(), + ) + + self.assertEqual(missing.status_code, 401) + self.assertEqual( + missing.json()["error"]["code"], + "authentication_required", + ) + self.assertEqual(missing.headers["www-authenticate"], "Bearer") + self.assertEqual(accepted.status_code, 200) + self.assertEqual(accepted.json(), {"items": []}) + context.application.list_capabilities.assert_called_once_with() + + def test_repository_scopes_are_enforced_per_operation(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.static, + ) + authenticator = Mock() + authenticator.authenticate.return_value = Principal( + subject="reader", + scopes=frozenset({"vidxp.read"}), + ) + authenticator.readiness.return_value = ComponentReadiness( + name="authentication", + ready=True, + message="Authentication is ready.", + ) + context = replace(context, authenticator=authenticator) + context.application.list_capabilities.return_value = () + with TestClient(create_app(context=context)) as client: + readable = client.get( + "/api/v1/capabilities", + headers=self.auth(), + ) + forbidden = client.post( + "/api/v1/jobs/index", + headers={ + **self.auth(), + "Idempotency-Key": IDEMPOTENCY_KEY, + }, + json={ + "media_id": MEDIA_ID, + "modalities": ["scene"], + "frame_stride": 1, + "capability_options": {}, + }, + ) + + self.assertEqual(readable.status_code, 200) + self.assertEqual(forbidden.status_code, 403) + self.assertEqual( + forbidden.json()["error"]["code"], + "insufficient_scope", + ) + context.jobs.submit_index.assert_not_called() + + def test_validation_errors_use_safe_shared_envelope(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + with TestClient(create_app(context=context)) as client: + response = client.get("/api/v1/media/not-an-id") + + self.assertEqual(response.status_code, 422) + error = response.json()["error"] + self.assertEqual(error["code"], "invalid_request") + self.assertEqual( + error["details"]["errors"][0]["location"], + ["path", "media_id"], + ) + self.assertTrue(error["correlation_id"]) + + def test_unexpected_errors_are_logged_and_masked(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.get_media.side_effect = RuntimeError( + "secret-path" + ) + with TestClient( + create_app(context=context), + raise_server_exceptions=False, + ) as client: + response = client.get(f"/api/v1/media/{MEDIA_ID}") + + self.assertEqual(response.status_code, 500) + payload = response.json() + self.assertEqual(payload["error"]["code"], "internal_error") + self.assertNotIn("secret-path", response.text) + + def test_job_submission_is_thin_idempotent_delegation(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.submit_index.return_value = queued_job() + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/jobs/index", + headers={"Idempotency-Key": IDEMPOTENCY_KEY}, + json={ + "media_id": MEDIA_ID, + "modalities": ["scene"], + "frame_stride": 1, + "scene_sample_fps": 0.5, + "capability_options": {}, + }, + ) + + self.assertEqual(response.status_code, 202) + self.assertEqual(response.headers["location"], f"/api/v1/jobs/{JOB_ID}") + command = context.jobs.submit_index.call_args.args[0] + self.assertEqual(command.media_id, MEDIA_ID) + self.assertEqual(command.modalities, ("scene",)) + self.assertEqual(command.scene_sample_fps, 0.5) + context.application.require_models.assert_called_once_with(("scene",)) + expected_job_id = scoped_job_id( + context, + context.authenticator.authenticate(None), + operation="index", + idempotency_key=IDEMPOTENCY_KEY, + ) + self.assertEqual( + context.jobs.submit_index.call_args.kwargs["job_id"], + expected_job_id, + ) + self.assertNotEqual(expected_job_id, IDEMPOTENCY_KEY) + self.assertNotEqual( + expected_job_id, + scoped_job_id( + context, + Principal(subject="other", scopes=frozenset({"*"})), + operation="index", + idempotency_key=IDEMPOTENCY_KEY, + ), + ) + self.assertNotEqual( + expected_job_id, + scoped_job_id( + context, + context.authenticator.authenticate(None), + operation="snippet", + idempotency_key=IDEMPOTENCY_KEY, + ), + ) + + def test_missing_models_fail_before_job_submission(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.require_models.side_effect = ApplicationError( + "model_unavailable", + ErrorCategory.unavailable, + "Run vidxp prepare --modalities scene.", + details={ + "capability": "scene", + "remediation": "vidxp prepare --modalities scene", + }, + ) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/jobs/index", + headers={"Idempotency-Key": IDEMPOTENCY_KEY}, + json={ + "media_id": MEDIA_ID, + "modalities": ["scene"], + }, + ) + + self.assertEqual(response.status_code, 503) + self.assertEqual(response.json()["error"]["code"], "model_unavailable") + self.assertEqual( + response.json()["error"]["details"]["remediation"], + "vidxp prepare --modalities scene", + ) + context.jobs.submit_index.assert_not_called() + + def test_job_submission_requires_an_idempotency_key(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/jobs/index", + json={ + "media_id": MEDIA_ID, + "modalities": ["scene"], + "frame_stride": 1, + "capability_options": {}, + }, + ) + + self.assertEqual(response.status_code, 422) + context.jobs.submit_index.assert_not_called() + + def test_search_submission_uses_the_durable_query_boundary(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.submit_search.return_value = queued_job().model_copy( + update={"kind": JobKind.search} + ) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/jobs/search", + headers={"Idempotency-Key": IDEMPOTENCY_KEY}, + json={ + "modalities": ["scene"], + "query": "yellow taxi", + "top_k": 3, + }, + ) + + self.assertEqual(response.status_code, 202) + context.jobs.submit_search.assert_called_once() + command = context.jobs.submit_search.call_args.args[0] + self.assertEqual( + command, + SearchCommand( + modalities=("scene",), + query="yellow taxi", + top_k=3, + ), + ) + + def test_grounded_query_submission_uses_the_durable_boundary(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.submit_query.return_value = queued_job().model_copy( + update={"kind": JobKind.query} + ) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/jobs/query", + headers={"Idempotency-Key": IDEMPOTENCY_KEY}, + json={ + "question": "What happens after the taxi arrives?", + "media_id": MEDIA_ID, + "modalities": ["scene", "dialogue"], + "top_k": 5, + }, + ) + + self.assertEqual(response.status_code, 202) + context.jobs.submit_query.assert_called_once() + command = context.jobs.submit_query.call_args.args[0] + self.assertEqual( + command, + QueryVideoCommand( + question="What happens after the taxi arrives?", + media_id=MEDIA_ID, + modalities=("scene", "dialogue"), + top_k=5, + ), + ) + + def test_idempotency_payload_mismatch_returns_422(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.submit_index.side_effect = ApplicationError( + "idempotency_key_reused", + ErrorCategory.validation, + "The idempotency key was used for another request.", + ) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/jobs/index", + headers={"Idempotency-Key": IDEMPOTENCY_KEY}, + json={ + "media_id": MEDIA_ID, + "modalities": ["scene"], + "frame_stride": 1, + "capability_options": {}, + }, + ) + + self.assertEqual(response.status_code, 422) + self.assertEqual( + response.json()["error"]["code"], + "idempotency_key_reused", + ) + + def test_small_upload_stages_then_calls_application_ingestion(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory), upload_limit=32) + captured = {} + + def ingest(**kwargs): + staged = kwargs["staged_path"] + captured["path"] = staged + captured["bytes"] = staged.read_bytes() + captured["filename"] = kwargs["original_filename"] + captured["mime"] = kwargs["declared_mime_type"] + captured["request_key"] = kwargs["request_key"] + return media_asset() + + context.application.import_uploaded_media.side_effect = ingest + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/media", + headers={"Idempotency-Key": IDEMPOTENCY_KEY}, + files={"upload": ("video.mp4", b"0123456789", "video/mp4")}, + ) + + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json()["media_id"], MEDIA_ID) + self.assertEqual(captured["bytes"], b"0123456789") + self.assertEqual(captured["filename"], "video.mp4") + self.assertEqual(captured["mime"], "video/mp4") + self.assertEqual(len(captured["request_key"]), 64) + self.assertFalse(captured["path"].exists()) + + def test_small_upload_enforces_streamed_file_limit(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory), upload_limit=4) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/media", + headers={"Idempotency-Key": IDEMPOTENCY_KEY}, + files={"upload": ("video.mp4", b"12345", "video/mp4")}, + ) + + self.assertEqual(response.status_code, 413) + self.assertEqual(response.json()["error"]["code"], "media_too_large") + context.application.import_uploaded_media.assert_not_called() + + def test_json_body_limit_rejects_before_job_dispatch(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory), json_limit=32) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/api/v1/jobs/index", + json={ + "media_id": MEDIA_ID, + "modalities": ["scene"], + "frame_stride": 1, + "capability_options": {}, + }, + ) + + self.assertEqual(response.status_code, 413) + self.assertEqual( + response.json()["error"]["code"], + "request_body_too_large", + ) + context.jobs.submit_index.assert_not_called() + + def test_streamed_body_limit_does_not_require_content_length(self): + async def request() -> httpx.Response: + with TemporaryDirectory() as directory: + context = self.context(Path(directory), upload_limit=4) + app = create_app(context=context) + + async def body(): + yield ( + b"--boundary\r\n" + b'Content-Disposition: form-data; name="upload"; ' + b'filename="video.mp4"\r\n' + b"Content-Type: video/mp4\r\n\r\n" + ) + yield b"x" * (1024 * 1024 + 32) + yield b"\r\n--boundary--\r\n" + + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient( + transport=transport, + base_url="http://testserver", + ) as client: + return await client.post( + "/api/v1/media", + headers={ + "Content-Type": ( + "multipart/form-data; boundary=boundary" + ), + "Idempotency-Key": IDEMPOTENCY_KEY, + }, + content=body(), + ) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", ResourceWarning) + response = asyncio.run(request()) + gc.collect() + + self.assertEqual(response.status_code, 413) + self.assertEqual( + response.json()["error"]["code"], + "request_body_too_large", + ) + self.assertFalse( + [ + warning + for warning in caught + if issubclass(warning.category, ResourceWarning) + and "Unclosed file" in str(warning.message) + ] + ) + + def test_file_delivery_uses_starlette_range_and_strong_etag(self): + with TemporaryDirectory() as directory: + root = Path(directory) + content = root / "video.mp4" + content.write_bytes(b"0123456789") + context = self.context(root) + context.application.open_media_content.return_value = ( + LocalFileResource( + path=content, + filename="video.mp4", + mime_type="video/mp4", + byte_size=10, + etag="1" * 64, + ) + ) + context.application.open_artifact_content.return_value = ( + LocalFileResource( + path=content, + filename="snippet.mp4", + mime_type="video/mp4", + byte_size=10, + etag="1" * 64, + ) + ) + with TestClient(create_app(context=context)) as client: + ranged = client.get( + f"/api/v1/media/{MEDIA_ID}/content", + headers={"Range": "bytes=2-5"}, + ) + cached = client.get( + f"/api/v1/media/{MEDIA_ID}/content", + headers={"If-None-Match": f'"{"1" * 64}"'}, + ) + artifact = client.get( + f"/api/v1/artifacts/{ARTIFACT_ID}/content", + headers={"Range": "bytes=6-9"}, + ) + + self.assertEqual(ranged.status_code, 206) + self.assertEqual(ranged.content, b"2345") + self.assertEqual(ranged.headers["content-range"], "bytes 2-5/10") + self.assertEqual(ranged.headers["etag"], f'"{"1" * 64}"') + self.assertEqual(cached.status_code, 304) + self.assertEqual(cached.content, b"") + self.assertEqual(artifact.status_code, 206) + self.assertEqual(artifact.content, b"6789") + self.assertIn( + "snippet.mp4", + artifact.headers["content-disposition"], + ) + + def test_openapi_is_curated_and_has_unique_operation_ids(self): + with TemporaryDirectory() as directory: + app = create_app(context=self.context(Path(directory))) + schema = app.openapi() + + operations = [ + operation + for path in schema["paths"].values() + for method, operation in path.items() + if method.lower() in { + "get", + "post", + "put", + "patch", + "delete", + "head", + } + ] + operation_ids = [operation["operationId"] for operation in operations] + self.assertEqual(len(operation_ids), len(set(operation_ids))) + self.assertNotIn("/api/v1/execute", schema["paths"]) + self.assertNotIn("/mcp", schema["paths"]) + upload_schema = schema["paths"]["/api/v1/media"]["post"] + self.assertIn("multipart/form-data", upload_schema["requestBody"]["content"]) + self.assertNotIn('"format": "path"', str(upload_schema)) + clip_schema = schema["paths"]["/api/v1/jobs/snippet"]["post"] + self.assertIn("downloadable", clip_schema["summary"].lower()) + artifact_schema = schema["paths"][ + "/api/v1/artifacts/{artifact_id}/content" + ]["get"] + self.assertIn("download", artifact_schema["summary"].lower()) + + def test_openapi_declares_bearer_security_without_securing_health(self): + with TemporaryDirectory() as directory: + app = create_app( + context=self.context( + Path(directory), + auth=HttpAuthMode.static, + ) + ) + schema = app.openapi() + + bearer = schema["components"]["securitySchemes"]["BearerAuth"] + self.assertEqual(bearer["type"], "http") + self.assertEqual(bearer["scheme"], "bearer") + self.assertEqual( + schema["paths"]["/api/v1/capabilities"]["get"]["security"], + [{"BearerAuth": []}], + ) + self.assertNotIn("security", schema["paths"]["/health"]["get"]) + + def test_cors_is_scoped_to_the_rest_namespace(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + allowed_origins=("https://client.example",), + ) + headers = { + "Origin": "https://client.example", + "Access-Control-Request-Method": "GET", + } + with TestClient(create_app(context=context)) as client: + api = client.options("/api/v1/media", headers=headers) + mcp = client.options("/mcp", headers=headers) + + self.assertEqual(api.status_code, 200) + self.assertEqual( + api.headers["access-control-allow-origin"], + "https://client.example", + ) + self.assertNotIn("access-control-allow-origin", mcp.headers) + + def test_security_middleware_rejections_use_typed_envelopes(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + allowed_origins=("https://client.example",), + ) + with TestClient(create_app(context=context)) as client: + host = client.get( + "/health", + headers={"Host": "untrusted.example"}, + ) + ipv6_loopback = client.get( + "/health", + headers={"Host": "[::1]:8000"}, + ) + origin = client.options( + "/api/v1/media", + headers={ + "Origin": "https://untrusted.example", + "Access-Control-Request-Method": "GET", + }, + ) + method = client.options( + "/api/v1/media", + headers={ + "Origin": "https://client.example", + "Access-Control-Request-Method": "TRACE", + }, + ) + + self.assertEqual(host.status_code, 400) + self.assertEqual(host.json()["error"]["code"], "host_not_allowed") + self.assertEqual(ipv6_loopback.status_code, 200) + self.assertEqual(origin.status_code, 403) + self.assertEqual( + origin.json()["error"]["code"], + "cors_origin_forbidden", + ) + self.assertEqual(method.status_code, 400) + self.assertEqual( + method.json()["error"]["code"], + "cors_preflight_invalid", + ) + + def test_authenticated_profiles_protect_schema_and_hide_interactive_docs(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.static, + ) + with TestClient(create_app(context=context)) as client: + unauthenticated = client.get("/docs") + authenticated = client.get( + "/docs", + headers=self.auth(), + ) + schema_without_token = client.get("/openapi.json") + schema = client.get( + "/openapi.json", + headers=self.auth(), + ) + + self.assertEqual(unauthenticated.status_code, 401) + self.assertEqual(authenticated.status_code, 404) + self.assertEqual(schema_without_token.status_code, 401) + self.assertEqual(schema.status_code, 200) + self.assertIn("/api/v1/capabilities", schema.json()["paths"]) + + def test_readiness_masks_component_probe_failures(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.control_plane_readiness.side_effect = OSError( + "private-catalog-detail" + ) + context.application.runtime_readiness.side_effect = OSError( + "private-runtime-detail" + ) + context.jobs.readiness.return_value = ComponentReadiness( + name="workflow", + ready=True, + message="Workflow is ready.", + ) + readiness = ReadinessService( + application=context.application, + jobs=context.jobs, + authenticator=context.authenticator, + ) + context = replace(context, readiness=readiness) + with TestClient(create_app(context=context)) as client: + minimal = client.get("/ready") + details = client.get("/api/v1/runtime/readiness") + + self.assertEqual(minimal.status_code, 503) + self.assertEqual( + minimal.json(), + {"ready": False, "status": "not_ready"}, + ) + self.assertEqual(details.status_code, 200) + self.assertFalse(details.json()["ready"]) + self.assertNotIn("private-", details.text) + + def test_owned_context_is_closed_when_lifespan_exits(self): + with TemporaryDirectory() as directory: + owned = self.context(Path(directory)) + with patch( + "vidxp.api.create_http_application", + return_value=owned, + ): + with TestClient(create_app()) as client: + self.assertEqual(client.get("/health").status_code, 200) + + owned.jobs.close.assert_called_once_with() + + def test_local_worker_startup_failure_fails_api_startup(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.start.side_effect = RuntimeError( + "transient worker failure" + ) + + with self.assertRaisesRegex( + RuntimeError, + "transient worker failure", + ): + with TestClient(create_app(context=context)): + pass + + context.jobs.start.assert_called_once_with() + + def test_http_composition_does_not_construct_model_runtime(self): + with TemporaryDirectory(ignore_cleanup_errors=True) as directory: + settings = VidXPSettings( + repository_root=Path(directory), + runtime_backend="cpu", + ) + with patch( + "vidxp.composition.ModelRuntime", + side_effect=AssertionError("model runtime constructed"), + ) as runtime: + context = create_http_application(settings) + try: + self.assertIsInstance( + context.application, + ControlPlaneApplication, + ) + runtime.assert_not_called() + finally: + context.close() + + def test_mcp_namespace_uses_static_auth_and_sdk_body_policy(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.static, + mcp_limit=32, + ) + with TestClient(create_app(context=context)) as client: + unauthorized = client.post( + "/mcp", + headers={"Content-Type": "application/json"}, + content=b"{}", + ) + oversized = client.post( + "/mcp", + headers={ + **self.auth(), + "Content-Type": "application/json", + }, + content=b"x" * 64, + ) + metadata = client.get( + "/.well-known/oauth-protected-resource/mcp" + ) + + self.assertEqual(unauthorized.status_code, 401) + self.assertEqual( + unauthorized.json()["error"]["code"], + "authentication_required", + ) + self.assertEqual(oversized.status_code, 413) + self.assertEqual(metadata.status_code, 401) + self.assertEqual( + metadata.json()["error"]["code"], + "authentication_required", + ) + + def test_oidc_mcp_metadata_is_public_and_sdk_auth_challenges(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.oidc, + ) + with TestClient(create_app(context=context)) as client: + metadata = client.get( + "/.well-known/oauth-protected-resource/mcp" + ) + challenged = client.post( + "/mcp", + headers={ + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2026-07-28", + "capabilities": {}, + "clientInfo": { + "name": "test", + "version": "1", + }, + }, + }, + ) + + self.assertEqual(metadata.status_code, 200) + self.assertEqual( + metadata.json()["resource"], + "https://api.example/mcp", + ) + self.assertEqual( + metadata.json()["authorization_servers"], + ["https://issuer.example"], + ) + self.assertEqual(challenged.status_code, 401) + self.assertIn( + "/.well-known/oauth-protected-resource/mcp", + challenged.headers["www-authenticate"], + ) + + def test_oidc_mcp_valid_token_with_missing_scope_returns_403(self): + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + public_jwk = jwt.PyJWK.from_dict( + { + **jwt.algorithms.RSAAlgorithm.to_jwk( + private_key.public_key(), + as_dict=True, + ), + "kid": "mcp-key", + "alg": "RS256", + "use": "sig", + } + ) + now = datetime.now(timezone.utc) + token = jwt.encode( + { + "sub": "user-1", + "iss": "https://issuer.example", + "aud": "https://api.example/mcp", + "scope": "other", + "iat": now, + "exp": now.replace(year=now.year + 1), + }, + private_key, + algorithm="RS256", + headers={"kid": "mcp-key"}, + ) + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + auth=HttpAuthMode.oidc, + ) + context.authenticator._jwks = Mock() + context.authenticator._jwks.get_signing_key_from_jwt.return_value = ( + public_jwk + ) + with TestClient(create_app(context=context)) as client: + response = client.post( + "/mcp", + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2026-07-28", + "capabilities": {}, + "clientInfo": { + "name": "test", + "version": "1", + }, + }, + }, + ) + + self.assertEqual(response.status_code, 403) + + def test_typed_application_error_status_mapping(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.get_media.side_effect = ApplicationError( + "media_conflict", + ErrorCategory.conflict, + "The media is in conflict.", + ) + with TestClient(create_app(context=context)) as client: + response = client.get(f"/api/v1/media/{MEDIA_ID}") + + self.assertEqual(response.status_code, 409) + self.assertEqual(response.json()["error"]["code"], "media_conflict") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_api_cli.py b/tests/test_api_cli.py new file mode 100644 index 0000000..60682e6 --- /dev/null +++ b/tests/test_api_cli.py @@ -0,0 +1,20 @@ +import unittest +from contextlib import redirect_stdout +from io import StringIO + +from vidxp.api_cli import main + + +class ApiCliTests(unittest.TestCase): + def test_help_exits_without_starting_the_service(self): + output = StringIO() + + with redirect_stdout(output), self.assertRaises(SystemExit) as caught: + main(["--help"]) + + self.assertEqual(caught.exception.code, 0) + self.assertIn("VIDXP_* environment variables", output.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cli.py b/tests/test_cli.py index 9e996e7..b3b4466 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,426 +1,1006 @@ import json import os -import sys import unittest -from contextlib import redirect_stderr -from io import StringIO +from datetime import datetime, timezone from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import Mock, patch +from click import unstyle from typer.testing import CliRunner from vidxp import cli -from vidxp.capabilities.actor.schemas import ( - ActorClusterSummary, - ActorDetection, - ActorRenderResult, +from vidxp.entrypoint import startup_command +from vidxp.composition import LocalApplicationContext, settings_for_repository +from vidxp.media_runtime import ( + MediaRuntimeConfiguration, + MediaRuntimeStatus, + SystemInstallPlan, ) -from vidxp.capabilities.schemas import SearchHit, SearchResult -from vidxp.index_state import IndexNotReadyError - - -def result(modality, starts): - return SearchResult( - query_id="q", - query="example", - modality=modality, - hits=tuple( - SearchHit( - rank=index + 1, - video_id="video-1", - start=start, - end=start + 1, - score=-float(index), - raw_distance=float(index), - modality=modality, - source_id=f"source-{index}", - ) - for index, start in enumerate(starts) - ), - ) +from vidxp.settings import ApplicationMode +from vidxp.application_models import ( + Artifact, + ArtifactJobResult, + CapabilityDependencyCheck, + CreateIndexCommand, + DependencyCheckResult, + DependencyKind, + FusedSearchResult, + FusionProvenance, + IndexJobResult, + IndexResult, + IndexStatus, + IndexStatusSummary, + Job, + JobKind, + JobQueue, + JobState, + MediaAsset, + PrepareModelsResult, + PrepareModelsJobResult, + QueryAnswer, + QueryAnswerMode, + QueryJobResult, + QueryPlan, + QueryVideoCommand, + RemoveIndexCommand, + SearchCommand, + SearchJobResult, + SearchMomentsPlanStep, +) +from vidxp.core.artifacts import ArtifactKind, ArtifactState +from vidxp.core.media import MediaState, MediaStream +from vidxp.capabilities.registry import create_capability_registry +from vidxp.capability_service import CapabilityService +from vidxp.repositories import RepositoryConfig, RepositoryRegistry +from vidxp.ports import LocalFileResource + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" +JOB_ID = "423456781234423481234567890abcde" +ARTIFACT_ID = "523456781234423481234567890abcde" class CliTests(unittest.TestCase): def setUp(self): self.runner = CliRunner() - self.temporary_directory = TemporaryDirectory() - self.addCleanup(self.temporary_directory.cleanup) - self.config_file = ( - Path(self.temporary_directory.name) / "repositories.json" - ) self.service = Mock() - self.service.index_directory = Path("chroma_data") - self.service.device = None - - def invoke(self, arguments): - with patch.object( - cli, - "VidXPService", - return_value=self.service, + self.service.registry = create_capability_registry() + self.service.list_capabilities.return_value = CapabilityService( + self.service.registry + ).list() + self.service.index_directory = Path("repo/indexes") + self.service.layout.root = Path("repo") + self.service.model_cache = Path("model-cache") + self.service.runtime.backends.requested = "cpu" + self.service.model_readiness.return_value = DependencyCheckResult( + ok=True, + modalities=(), + checks=(), + ) + self.jobs = Mock() + self.registry = Mock(spec=RepositoryRegistry) + self.registry.path = Path("repositories.json") + self.repository = RepositoryConfig( + "default", + Path("repo"), + device="cpu", + configured=False, + ) + + def invoke(self, arguments, *, media_runtime_initialized=True): + with ( + patch.object( + cli, + "create_local_application", + return_value=LocalApplicationContext( + application=self.service, + jobs=self.jobs, + repositories=self.registry, + repository=self.repository, + ), + ) as create_local_application, + patch( + "vidxp.cli_support.media_runtime_is_initialized", + return_value=media_runtime_initialized, + ), ): - return self.runner.invoke( - cli.app, - ["--config", str(self.config_file), *arguments], - ) + result = self.runner.invoke(cli.app, arguments) + self.create_local_application = create_local_application + return result - def test_grouped_commands_are_exposed(self): - response = self.invoke(["--help"]) + def test_data_directory_is_forwarded_to_local_composition(self): + result = self.invoke( + [ + "--data-dir", + "custom-data", + "repositories", + "show", + "--json", + ] + ) - self.assertEqual(response.exit_code, 0) + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual( + self.create_local_application.call_args.kwargs["data_directory"], + Path("custom-data"), + ) + + def test_grouped_commands_are_exposed(self): + result = self.invoke(["--help"]) + self.assertEqual(result.exit_code, 0, result.output) for command in ( + "media", "index", + "jobs", "search", "actors", - "repositories", - "benchmark", + "artifacts", + "init", + "doctor", + "prepare", + "mcp-config", + ): + self.assertIn(command, result.output) + + def test_mcp_config_is_copy_paste_json_without_opening_repository(self): + result = self.invoke(["mcp-config"]) + + self.assertEqual(result.exit_code, 0, result.output) + self.create_local_application.assert_not_called() + config = json.loads(result.output) + server = config["mcpServers"]["vidxp"] + self.assertIn( + Path(server["command"]).name.lower(), + {"vidxp-mcp", "vidxp-mcp.exe"}, + ) + self.assertEqual(server["args"], ["--repository", "default"]) + + def test_startup_notice_targets_long_interactive_commands(self): + self.assertEqual(startup_command(["init"]), "init") + self.assertEqual(startup_command(["doctor"]), "doctor") + self.assertEqual( + startup_command(["media", "import", "video.mp4"]), + "media import", + ) + self.assertEqual( + startup_command(["--data-dir", "custom-data", "ui"]), "ui", + ) + self.assertIsNone(startup_command(["doctor", "--json"])) + self.assertIsNone(startup_command(["--quiet", "prepare"])) + self.assertIsNone(startup_command(["repositories", "list"])) + + def test_init_saves_verified_paths_without_opening_a_repository(self): + ffmpeg = Path("tools/ffmpeg.exe").resolve() + ffprobe = Path("tools/ffprobe.exe").resolve() + status = MediaRuntimeStatus( + ready=True, + initialized=False, + ffmpeg_executable=ffmpeg, + ffprobe_executable=ffprobe, + ) + configuration = MediaRuntimeConfiguration( + ffmpeg_executable=ffmpeg, + ffprobe_executable=ffprobe, + ) + with ( + patch( + "vidxp.cli_commands.runtime.inspect_media_runtime", + return_value=status, + ), + patch( + "vidxp.cli_commands.runtime.save_media_runtime_configuration", + return_value=configuration, + ) as save, + patch( + "vidxp.cli_commands.runtime.media_runtime_config_path", + return_value=Path("config/media-runtime.json").resolve(), + ), ): - self.assertIn(command, response.stdout) + result = self.invoke(["init", "--json"]) + + self.assertEqual(result.exit_code, 0, result.output) + payload = json.loads(result.output) + self.assertTrue(payload["ready"]) + self.assertTrue(payload["initialized"]) + save.assert_called_once_with(status) + self.create_local_application.assert_not_called() + + def test_noninteractive_init_reports_command_without_installing(self): + plan = SystemInstallPlan( + manager="Windows Package Manager", + command=( + "winget", + "install", + "--id", + "Gyan.FFmpeg", + "--exact", + ), + automatic=True, + ) + status = MediaRuntimeStatus( + ready=False, + initialized=False, + errors=("FFmpeg was not found.",), + install_plan=plan, + ) + with ( + patch( + "vidxp.cli_commands.runtime.inspect_media_runtime", + return_value=status, + ), + patch( + "vidxp.cli_commands.runtime.install_media_runtime", + ) as install, + ): + result = self.invoke(["init", "--json"]) + + self.assertEqual(result.exit_code, 1, result.output) + payload = json.loads(result.output) + self.assertFalse(payload["ready"]) + self.assertIn("Gyan.FFmpeg", payload["install_command"]) + install.assert_not_called() + self.create_local_application.assert_not_called() + + def test_yes_explicitly_runs_the_displayed_installer_and_verifies(self): + plan = SystemInstallPlan( + manager="Windows Package Manager", + command=("winget", "install", "--id", "Gyan.FFmpeg"), + automatic=True, + ) + missing = MediaRuntimeStatus( + ready=False, + initialized=False, + errors=("FFmpeg was not found.",), + install_plan=plan, + ) + ready = MediaRuntimeStatus( + ready=True, + initialized=False, + ffmpeg_executable=Path("tools/ffmpeg.exe").resolve(), + ffprobe_executable=Path("tools/ffprobe.exe").resolve(), + ) + configuration = MediaRuntimeConfiguration( + ffmpeg_executable=ready.ffmpeg_executable, + ffprobe_executable=ready.ffprobe_executable, + ) + with ( + patch( + "vidxp.cli_commands.runtime.inspect_media_runtime", + side_effect=(missing, ready), + ), + patch( + "vidxp.cli_commands.runtime.install_media_runtime", + ) as install, + patch( + "vidxp.cli_commands.runtime.save_media_runtime_configuration", + return_value=configuration, + ), + ): + result = self.invoke(["init", "--yes", "--json"]) + + self.assertEqual(result.exit_code, 0, result.output) + self.assertTrue(json.loads(result.output)["ready"]) + install.assert_called_once_with(plan, output_to_stderr=True) + self.create_local_application.assert_not_called() + + def test_first_media_command_points_to_init_when_uninitialized(self): + with TemporaryDirectory() as directory: + video = Path(directory) / "video.mp4" + video.write_bytes(b"video") + result = self.invoke( + ["media", "import", str(video)], + media_runtime_initialized=False, + ) - def test_removed_legacy_commands_are_rejected(self): - for command in ("videoindex", "dialogue", "scene", "actor"): - with self.subTest(command=command): - response = self.invoke([command]) + self.assertEqual(result.exit_code, 1) + self.assertEqual(result.exception.code, "media_runtime_uninitialized") + self.assertIn("vidxp init", str(result.exception)) + self.service.import_media.assert_not_called() - self.assertEqual(response.exit_code, 2) - self.assertIn("No such command", response.output) + def test_ui_shutdown_stops_its_local_worker(self): + with patch( + "vidxp.frontend.main", + side_effect=SystemExit(0), + ): + result = self.invoke(["ui"]) + + self.assertEqual(result.exit_code, 0, result.output) + self.jobs.stop_worker.assert_called_once_with() - def test_search_returns_ranked_json_and_passes_top_k(self): - self.service.search.return_value = result( - "dialogue", - [7.5, 12.0], + def test_snippet_rejects_an_inverted_time_range_before_submission(self): + result = self.invoke( + ["artifacts", "snippet", MEDIA_ID, "3", "2"] ) - response = self.invoke( - [ - "search", - "dialogue", - "fresh bread", - "--top-k", - "2", - "--json", - ] + self.assertEqual(result.exit_code, 2, result.output) + self.assertIn("snippet end must be greater than its", result.output) + self.assertIn("start.", result.output) + self.jobs.submit_snippet.assert_not_called() + + def test_snippet_completion_points_to_the_download_command(self): + artifact = Artifact( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="compatible_mp4", + mime_type="video/mp4", + byte_size=12, + sha256="1" * 64, + state=ArtifactState.ready, + created_at=datetime.now(timezone.utc), + ) + self.jobs.submit_snippet.return_value = Job( + job_id=JOB_ID, + kind=JobKind.snippet, + state=JobState.queued, + queue=JobQueue.cpu, + ) + self.jobs.wait.return_value = Job( + job_id=JOB_ID, + kind=JobKind.snippet, + state=JobState.succeeded, + queue=JobQueue.cpu, + result=ArtifactJobResult( + kind=JobKind.snippet, + result=artifact, + ), ) - self.assertEqual(response.exit_code, 0, response.output) - payload = json.loads(response.stdout) - self.assertEqual( - [hit["start"] for hit in payload["hits"]], - [7.5, 12.0], - ) - self.service.search.assert_called_once_with( - "dialogue", - "fresh bread", - top_k=2, - ) - - def test_global_index_directory_and_device_configure_service(self): - with patch.object(cli, "VidXPService") as service_type: - service_type.return_value.index_status.return_value = { - "state": "missing", - } - response = self.runner.invoke( - cli.app, + result = self.invoke( + ["artifacts", "snippet", MEDIA_ID, "9", "17"] + ) + + self.assertEqual(result.exit_code, 0, result.output) + self.assertIn(f"Clip ready: {ARTIFACT_ID}", result.output) + self.assertIn( + f"vidxp artifacts download {ARTIFACT_ID}", + result.output, + ) + + def test_artifact_download_copies_authorized_content(self): + with TemporaryDirectory() as directory: + root = Path(directory) + source = root / "managed.mp4" + source.write_bytes(b"clip-content") + destination = root / "exported.mp4" + self.service.open_artifact_content.return_value = ( + LocalFileResource( + path=source, + filename=f"snippet-{ARTIFACT_ID}.mp4", + mime_type="video/mp4", + byte_size=12, + etag="1" * 64, + ) + ) + + result = self.invoke( [ - "--config", - str(self.config_file), - "--index-dir", - "custom-index", - "--device", - "cuda", - "index", - "status", + "artifacts", + "download", + ARTIFACT_ID, + str(destination), "--json", - ], + ] ) - self.assertEqual(response.exit_code, 0, response.output) - service_type.assert_called_once_with(Path("custom-index"), device="cuda") + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(destination.read_bytes(), b"clip-content") + payload = json.loads(result.output) + self.assertEqual(payload["artifact_id"], ARTIFACT_ID) + self.assertEqual(payload["path"], str(destination.resolve())) + + refused = self.invoke( + [ + "artifacts", + "download", + ARTIFACT_ID, + str(destination), + ] + ) - def test_repository_commands_persist_and_select_named_indexes(self): - index_directory = ( - Path(self.temporary_directory.name) / "team-index" + self.assertEqual(refused.exit_code, 2, refused.output) + self.assertIn("already exists", refused.output) + + def test_search_constructs_shared_command(self): + search_result = FusedSearchResult( + query_id="fused:1", + query="yellow taxi", + modalities=("scene",), + fusion=FusionProvenance( + requested_modalities=("scene",), + searched_modalities=("scene",), + ), + ) + self.jobs.submit_search.return_value = Job( + job_id=JOB_ID, + kind=JobKind.search, + state=JobState.queued, + queue=JobQueue.cpu, ) - added = self.invoke( + self.jobs.wait.return_value = Job( + job_id=JOB_ID, + kind=JobKind.search, + state=JobState.succeeded, + queue=JobQueue.cpu, + result=SearchJobResult(result=search_result), + ) + + result = self.invoke( [ - "repositories", - "add", - "team", - "--index-dir", - str(index_directory), - "--device", - "cuda", - "--use", + "search", + "scene", + "yellow taxi", + "--media-id", + MEDIA_ID, + "--top-k", + "7", "--json", ] ) - listed = self.invoke(["repositories", "list", "--json"]) - self.assertEqual(added.exit_code, 0, added.output) - self.assertEqual(json.loads(added.stdout)["name"], "team") - payload = json.loads(listed.stdout) - self.assertEqual(payload["active_repository"], "team") - configured = { - item["name"]: item for item in payload["repositories"] - } - self.assertEqual( - configured["team"]["index_directory"], - str(index_directory.resolve()), + self.assertEqual(result.exit_code, 0, result.output) + self.jobs.submit_search.assert_called_once_with( + SearchCommand( + modalities=("scene",), + query="yellow taxi", + media_id=MEDIA_ID, + top_k=7, + ) + ) + self.assertEqual(json.loads(result.output)["query"], "yellow taxi") + + def test_search_help_explains_cross_media_default(self): + result = self.invoke(["search", "--help"]) + + self.assertEqual(result.exit_code, 0, result.output) + normalized = " ".join(unstyle(result.output).replace("│", " ").split()) + self.assertIn( + "Omit to rank matches across every media item in the active index snapshot.", + normalized, ) - def test_repository_removal_leaves_index_data_untouched(self): - index_directory = ( - Path(self.temporary_directory.name) / "team-index" + def test_query_constructs_shared_command_and_emits_typed_answer(self): + answer = QueryAnswer( + question="What happens?", + mode=QueryAnswerMode.no_evidence, + plan=QueryPlan( + steps=( + SearchMomentsPlanStep( + modality="scene", + query="What happens?", + ), + ) + ), + fusion=FusionProvenance( + requested_modalities=("scene",), + searched_modalities=("scene",), + ), + fallback_reason="no_evidence", + ) + self.jobs.submit_query.return_value = Job( + job_id=JOB_ID, + kind=JobKind.query, + state=JobState.queued, + queue=JobQueue.cpu, ) - index_directory.mkdir() - marker = index_directory / "keep" - marker.write_text("data", encoding="utf-8") - self.invoke( + self.jobs.wait.return_value = Job( + job_id=JOB_ID, + kind=JobKind.query, + state=JobState.succeeded, + queue=JobQueue.cpu, + result=QueryJobResult(result=answer), + ) + + result = self.invoke( [ - "repositories", - "add", - "team", - "--index-dir", - str(index_directory), + "query", + "What happens?", + "--media-id", + MEDIA_ID, + "--modality", + "scene", + "--json", ] ) - removed = self.invoke( - ["repositories", "remove", "team", "--yes", "--json"] + self.assertEqual(result.exit_code, 0, result.output) + self.jobs.submit_query.assert_called_once_with( + QueryVideoCommand( + question="What happens?", + media_id=MEDIA_ID, + modalities=("scene",), + ) + ) + self.assertEqual(json.loads(result.output)["mode"], "no_evidence") + + def test_index_constructs_shared_command(self): + result_value = IndexResult( + media_id=MEDIA_ID, + generation_id=GENERATION_ID, + snapshot_id=SNAPSHOT_ID, + active_media_count=1, + record_counts={"scene": 1}, + ) + self.jobs.submit_index.return_value = Job( + job_id=JOB_ID, + kind=JobKind.index, + state=JobState.queued, + queue=JobQueue.cpu, + ) + self.jobs.wait.return_value = Job( + job_id=JOB_ID, + kind=JobKind.index, + state=JobState.succeeded, + queue=JobQueue.cpu, + result=IndexJobResult(result=result_value), ) + result = self.invoke( + [ + "--format", + "json", + "index", + "create", + MEDIA_ID, + "--modality", + "scene", + "--frame-stride", + "5", + "--scene-sample-fps", + "2", + ] + ) + + self.assertEqual(result.exit_code, 0, result.output) + command = self.jobs.submit_index.call_args.args[0] + self.assertIsInstance(command, CreateIndexCommand) + self.assertEqual(command.modalities, ("scene",)) + self.assertEqual(command.frame_stride, 5) + self.assertEqual(command.scene_sample_fps, 2.0) + self.assertEqual(command.media_id, MEDIA_ID) + + def test_remove_uses_shared_media_id_command(self): + self.service.remove_from_index.return_value = True + removed = self.invoke(["index", "remove", MEDIA_ID, "--json"]) self.assertEqual(removed.exit_code, 0, removed.output) - self.assertFalse(json.loads(removed.stdout)["index_deleted"]) - self.assertTrue(marker.is_file()) + self.service.remove_from_index.assert_called_once_with( + RemoveIndexCommand(media_id=MEDIA_ID) + ) - def test_index_create_uses_repeated_typed_modalities(self): - self.service.create_index.return_value = {"scene_frames": 10} + def test_media_import_uses_the_local_import_command(self): + self.service.import_media.return_value = MediaAsset( + schema_version=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + original_filename="video.mp4", + sha256="1" * 64, + byte_size=5, + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=1, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + state=MediaState.ready, + created_at=datetime.now(timezone.utc), + ) with TemporaryDirectory() as directory: - video = Path(directory) / "sample.mp4" + video = Path(directory) / "video.mp4" video.write_bytes(b"video") - response = self.invoke( - [ - "--format", - "json", - "index", - "create", - str(video), - "--modality", - "scene", - "--frame-stride", - "5", - "--option", - "scene.batch_size=4", - "--option", - "scene.model=test-model", - ] - ) + result = self.invoke(["media", "import", str(video), "--json"]) - self.assertEqual(response.exit_code, 0, response.output) - self.assertEqual(json.loads(response.stdout), {"scene_frames": 10}) - self.service.create_index.assert_called_once() - call = self.service.create_index.call_args - self.assertEqual(call.kwargs["modalities"], ("scene",)) - self.assertEqual(call.kwargs["frame_stride"], 5) + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(json.loads(result.output)["media_id"], MEDIA_ID) self.assertEqual( - call.kwargs["capability_options"], - {"scene": {"batch_size": 4, "model": "test-model"}}, + self.service.import_media.call_args.args[0].path, + video.resolve(), ) - def test_index_status_reports_missing_index_as_json(self): - self.service.index_status.return_value = { - "state": "missing", - "message": "No local video index was found.", - } - - response = self.invoke(["index", "status", "--json"]) + def test_media_show_returns_registered_metadata(self): + self.service.get_media.return_value = MediaAsset( + schema_version=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + original_filename="video.mp4", + sha256="1" * 64, + byte_size=5, + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=1, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + state=MediaState.ready, + created_at=datetime.now(timezone.utc), + ) - self.assertEqual(response.exit_code, 0, response.output) - self.assertEqual(json.loads(response.stdout)["state"], "missing") + result = self.invoke(["media", "show", MEDIA_ID, "--json"]) - def test_index_clear_requires_explicit_confirmation_for_automation(self): - self.service.clear_index.return_value = True + self.assertEqual(result.exit_code, 0, result.output) + self.service.get_media.assert_called_once_with(MEDIA_ID) + self.assertEqual(json.loads(result.output)["media_id"], MEDIA_ID) - response = self.invoke(["index", "clear", "--yes", "--json"]) + def test_status_serializes_shared_model(self): + self.service.index_status.return_value = IndexStatus( + schema_version=1, + state="missing", + stage="status", + message="No index.", + ) - self.assertEqual(response.exit_code, 0, response.output) - self.assertTrue(json.loads(response.stdout)["cleared"]) - self.service.clear_index.assert_called_once_with() + result = self.invoke(["index", "status", "--json"]) + + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(json.loads(result.output)["state"], "missing") + + def test_index_list_joins_active_ids_to_registered_metadata(self): + self.service.index_status.return_value = IndexStatus( + schema_version=1, + state="ready", + stage="status", + message="Index ready.", + summary=IndexStatusSummary( + index_schema_version=1, + snapshot_id=SNAPSHOT_ID, + media_count=1, + media_ids=(MEDIA_ID,), + modalities=("scene", "dialogue"), + ), + ) + self.service.get_media.return_value = MediaAsset( + schema_version=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + original_filename="video.mp4", + sha256="1" * 64, + byte_size=5, + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=1, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + state=MediaState.ready, + created_at=datetime.now(timezone.utc), + ) - def test_doctor_and_prepare_use_the_reusable_service(self): - self.service.check_dependencies.return_value = { - "ok": True, - "modalities": ["scene"], - "checks": [{"name": "CLIP", "ok": True, "error": None}], - } - self.service.prepare_models.return_value = { - "prepared": ["ViT-B/32"], - "modalities": ["scene"], - "device": "cpu", - } + result = self.invoke(["index", "list", "--json"]) + + self.assertEqual(result.exit_code, 0, result.output) + self.service.get_media.assert_called_once_with(MEDIA_ID) + payload = json.loads(result.output) + self.assertEqual(payload["snapshot_id"], SNAPSHOT_ID) + self.assertEqual(payload["media_count"], 1) + self.assertEqual(payload["modalities"], ["scene", "dialogue"]) + self.assertEqual(payload["items"][0]["original_filename"], "video.mp4") + + def test_doctor_and_prepare_use_shared_models(self): + self.service.check_dependencies.return_value = DependencyCheckResult( + ok=True, + modalities=("scene",), + checks=(), + ) + prepared_value = PrepareModelsResult( + prepared=("scene-model",), + modalities=("scene",), + runtime={ + "requested": "cpu", + "torch_device": "cpu", + "transcription_device": "cpu", + }, + ) + self.jobs.submit_prepare_models.return_value = Job( + job_id=JOB_ID, + kind=JobKind.prepare_models, + state=JobState.queued, + queue=JobQueue.cpu, + ) + self.jobs.wait.return_value = Job( + job_id=JOB_ID, + kind=JobKind.prepare_models, + state=JobState.succeeded, + queue=JobQueue.cpu, + result=PrepareModelsJobResult(result=prepared_value), + ) checked = self.invoke( ["doctor", "--modalities", "scene", "--json"] ) - prepared = self.invoke([ - "prepare", - "--modalities", - "scene", - "--option", - "scene.model=test-model", - "--json", - ]) - - self.assertTrue(json.loads(checked.stdout)["ok"]) + prepared = self.invoke( + ["prepare", "--modalities", "scene", "--json"] + ) + + self.assertEqual(checked.exit_code, 0, checked.output) + self.assertEqual(prepared.exit_code, 0, prepared.output) self.assertEqual( - json.loads(prepared.stdout)["prepared"], - ["ViT-B/32"], + self.service.check_dependencies.call_args.args[0].modalities, + ("scene",), + ) + self.assertTrue( + self.service.check_dependencies.call_args.args[0].include_models ) - self.service.check_dependencies.assert_called_once_with(("scene",)) - self.service.prepare_models.assert_called_once() self.assertEqual( - self.service.prepare_models.call_args.kwargs[ - "capability_options" + self.jobs.submit_prepare_models.call_args.args[0].modalities, + ("scene",), + ) + + def test_doctor_accepts_repeated_modality_options(self): + self.service.check_dependencies.return_value = DependencyCheckResult( + ok=True, + modalities=("dialogue", "scene"), + checks=(), + ) + result = self.invoke( + [ + "doctor", + "--modalities", + "dialogue", + "--modalities", + "scene", + "--json", ], - {"scene": {"model": "test-model"}}, ) - def test_ui_receives_the_selected_service_configuration(self): - self.service.index_directory = Path("selected-index") - self.service.device = "cuda" - from vidxp import frontend + self.assertEqual(result.exit_code, 0, result.output) + command = self.service.check_dependencies.call_args.args[0] + self.assertEqual(command.modalities, ("dialogue", "scene")) + + def test_prepare_announces_start_and_subscribes_to_job_progress(self): + self.service.model_readiness.return_value = DependencyCheckResult( + ok=False, + modalities=("scene",), + checks=( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.model, + name="google/siglip2-base-patch16-224", + download_size_bytes=1_539_458_338, + ok=False, + error="model artifacts are not prepared", + ), + ), + ) + prepared = PrepareModelsResult( + prepared=("scene-model",), + modalities=("scene",), + runtime={ + "requested": "cpu", + "torch_device": "cpu", + "transcription_device": "cpu", + }, + ) + self.jobs.submit_prepare_models.return_value = Job( + job_id=JOB_ID, + kind=JobKind.prepare_models, + state=JobState.queued, + queue=JobQueue.cpu, + ) + self.jobs.wait.return_value = Job( + job_id=JOB_ID, + kind=JobKind.prepare_models, + state=JobState.succeeded, + queue=JobQueue.cpu, + result=PrepareModelsJobResult(result=prepared), + ) + + result = self.invoke( + ["prepare", "--modalities", "scene", "--yes"] + ) - with ( - patch.dict(os.environ, {}, clear=False), - patch.object(frontend, "SERVICE"), - patch.object(frontend, "SAVED_VIDEO_PATH"), - patch.object(frontend, "ACTOR_OUTPUT_PATH"), - patch.object(frontend, "main") as launch, + self.assertEqual(result.exit_code, 0, result.output) + self.assertIn("1.43 GiB", result.output) + self.assertRegex( + result.output, + r"\[\d{2}:\d{2}:\d{2}\] Starting model preparation for scene\.", + ) + self.assertTrue(callable(self.jobs.wait.call_args.kwargs["progress"])) + + def test_prepare_discloses_size_and_requires_confirmation(self): + self.service.model_readiness.return_value = DependencyCheckResult( + ok=False, + modalities=("scene",), + checks=( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.model, + name="google/siglip2-base-patch16-224", + download_size_bytes=1_539_458_338, + ok=False, + error="model artifacts are not prepared", + ), + ), + ) + + declined = self.invoke( + ["prepare", "--modalities", "scene"], + ) + + self.assertNotEqual(declined.exit_code, 0) + self.assertIn("1.43 GiB", declined.output) + self.assertIn("Model cache: model-cache", declined.output) + self.assertIn("Download these models?", declined.output) + self.jobs.submit_prepare_models.assert_not_called() + + def test_doctor_streams_timestamped_runtime_check_progress(self): + def check_dependencies( + _command, + *, + on_check_start, + on_check_complete, ): - response = self.invoke( - ["ui", "--host", "0.0.0.0", "--port", "8501"] + on_check_start( + "scene", + DependencyKind.distribution, + "torch", ) - - self.assertEqual(response.exit_code, 0, response.output) - launch.assert_called_once_with( - ["--server.address=0.0.0.0", "--server.port=8501"] + on_check_complete( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.distribution, + name="torch", + requirement="torch==2.13.0", + installed_version="2.13.0", + ok=True, + ), + 0.01, ) - self.assertEqual(os.environ["VIDXP_DEVICE"], "cuda") - self.assertEqual( - os.environ["VIDXP_INDEX_DIR"], - "selected-index", + on_check_start( + "scene", + DependencyKind.runtime, + "Torch import", ) - - def test_actor_commands_expose_clusters_detections_and_rendering(self): - cluster = ActorClusterSummary( - cluster_id="3", - video_id="video-1", - detection_count=4, - first_timestamp=1.0, - last_timestamp=8.0, - ) - self.service.actor_clusters.return_value = (cluster,) - self.service.actor_detections.return_value = [ - ActorDetection( - detection_id="d2", - cluster_id="3", - frame_index=2, - timestamp=1.5, - bbox=(1, 2, 3, 0), - dataset="local", - split="local", - run_id="default", - video_id="video-1", - modality="actor", - source_id="actor:d2", + on_check_complete( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.runtime, + name="Torch import", + ok=True, + ), + 1.25, ) - ] - self.service.render_actor.return_value = ActorRenderResult( - output_path=Path("actor.mp4"), - detection_count=4, - ) - - listed = self.invoke(["actors", "list", "--json"]) - inspected = self.invoke(["actors", "inspect", "3", "--json"]) - with TemporaryDirectory() as directory: - source = Path(directory) / "source.mp4" - source.write_bytes(b"video") - rendered = self.invoke( - [ - "actors", - "render", - "3", - str(source), - "--output", - "actor.mp4", - "--json", - ] + return DependencyCheckResult( + ok=True, + modalities=("scene",), + checks=( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.distribution, + name="torch", + requirement="torch==2.13.0", + installed_version="2.13.0", + ok=True, + ), + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.runtime, + name="Torch import", + ok=True, + ), + ), ) - self.assertEqual(json.loads(listed.stdout)["count"], 1) - self.assertEqual( - json.loads(inspected.stdout)["detection_count"], - 1, + self.service.check_dependencies.side_effect = check_dependencies + + result = self.invoke(["doctor", "--modalities", "scene"]) + + self.assertEqual(result.exit_code, 0, result.output) + self.assertRegex( + result.output, + r"\[\d{2}:\d{2}:\d{2}\] Checking \[scene\] Torch import\.\.\.", ) - self.assertEqual( - json.loads(rendered.stdout)["output_path"], - "actor.mp4", + self.assertIn("OK (1.2s)", result.output) + self.assertIn("OK (version 2.13.0, 0.0s)", result.output) + self.assertEqual(result.output.count("package torch"), 1) + + def test_doctor_prints_install_remedy_for_python_failures(self): + self.service.check_dependencies.return_value = DependencyCheckResult( + ok=False, + modalities=("scene",), + checks=( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.distribution, + name="transformers", + requirement="transformers>=5,<6", + ok=False, + error="distribution is not installed", + ), + ), ) - def test_benchmark_commands_are_exposed(self): - response = self.invoke(["benchmark", "--help"]) + result = self.invoke(["doctor", "--modalities", "scene"]) - self.assertEqual(response.exit_code, 0) - self.assertIn("didemo", response.stdout) - self.assertIn("hirest", response.stdout) + self.assertEqual(result.exit_code, 1, result.output) + self.assertIn( + "include --extra local-worker", + result.output, + ) + self.assertIn( + 'pip install "vidxp[scene]"', + result.output, + ) - def test_version_options_report_installed_package_version(self): - for option in ("--version", "-V"): - with self.subTest(option=option): - response = self.invoke([option]) + def test_doctor_reports_missing_models_and_prepare_command(self): + self.service.check_dependencies.return_value = DependencyCheckResult( + ok=False, + modalities=("scene",), + checks=( + CapabilityDependencyCheck( + capability="scene", + kind=DependencyKind.model, + name="google/siglip2-base-patch16-224", + download_size_bytes=1_539_458_338, + ok=False, + error="model artifacts are not prepared", + ), + ), + ) - self.assertEqual(response.exit_code, 0) - self.assertEqual( - response.stdout.strip(), - f"VidXP {cli.__version__}", - ) + result = self.invoke(["doctor", "--modalities", "scene"]) - def test_main_emits_uniform_json_for_runtime_errors(self): - self.service.search.side_effect = IndexNotReadyError( - "Index is not ready." - ) - stderr = StringIO() - arguments = [ - "vidxp", - "--config", - str(self.config_file), - "--format", - "json", - "search", - "scene", - "yellow taxi", - ] - with ( - patch.object(sys, "argv", arguments), - patch.object( - cli, - "VidXPService", - return_value=self.service, - ), - redirect_stderr(stderr), - self.assertRaises(SystemExit) as raised, + self.assertEqual(result.exit_code, 1, result.output) + self.assertIn( + "vidxp prepare --modalities scene", + result.output, + ) + self.assertIn("1.43 GiB", result.output) + self.assertNotIn("pip install", result.output) + + def test_invalid_capability_is_a_cli_parameter_error(self): + result = self.invoke( + ["doctor", "--modalities", "unknown", "--json"] + ) + self.assertNotEqual(result.exit_code, 0) + self.assertIn("Unknown or unsupported capabilities", result.output) + + def test_repository_without_device_preserves_runtime_environment(self): + repository = RepositoryConfig( + "default", + Path("repo"), + device=None, + configured=False, + ) + with patch.dict( + os.environ, + {"VIDXP_RUNTIME_BACKEND": "cpu"}, ): - cli.main() + settings = settings_for_repository(repository) - self.assertEqual(raised.exception.code, 1) - payload = json.loads(stderr.getvalue()) - self.assertFalse(payload["ok"]) - self.assertEqual( - payload["error"]["message"], - "Index is not ready.", + self.assertEqual(settings.runtime_backend, "cpu") + + def test_local_repository_ignores_server_mode_environment(self): + repository = RepositoryConfig( + "default", + Path("repo"), + device=None, + configured=False, ) - self.assertEqual(payload["error"]["exit_code"], 1) + with patch.dict(os.environ, {"VIDXP_MODE": "server"}): + settings = settings_for_repository(repository) + + self.assertEqual(settings.mode, ApplicationMode.local) if __name__ == "__main__": diff --git a/tests/test_database_cli.py b/tests/test_database_cli.py new file mode 100644 index 0000000..48c7284 --- /dev/null +++ b/tests/test_database_cli.py @@ -0,0 +1,37 @@ +import unittest +from unittest.mock import patch + +from alembic.config import Config +from alembic.script import ScriptDirectory + +from vidxp.database_cli import main +from vidxp.workflow_runtime import BUNDLED_POSTGRES_DATABASE_URL + + +class DatabaseCliTests(unittest.TestCase): + def test_migrations_use_the_bundled_server_database(self): + with patch("vidxp.database_cli.upgrade_database") as upgrade: + main([]) + + upgrade.assert_called_once_with(BUNDLED_POSTGRES_DATABASE_URL) + + def test_database_url_command_line_override_is_not_supported(self): + with self.assertRaises(SystemExit) as raised: + main(["--database-url", "postgresql://external.example/vidxp"]) + + self.assertEqual(raised.exception.code, 2) + + def test_single_repository_baseline_is_the_only_migration_head(self): + config = Config() + config.set_main_option( + "script_location", + "src/vidxp/migrations", + ) + + scripts = ScriptDirectory.from_config(config) + + self.assertEqual(scripts.get_heads(), ["20260729_01"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_frontend.py b/tests/test_frontend.py index d0afd30..ee426c4 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -1,258 +1,740 @@ import unittest +from contextlib import nullcontext +from types import SimpleNamespace from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import Mock, patch -from streamlit.testing.v1 import AppTest - from vidxp import frontend -from vidxp.capabilities.schemas import SearchHit, SearchResult -from vidxp.core.contracts import INDEX_SCHEMA_VERSION -from vidxp.index_state import IndexNotReadyError - - -READY_STATUS = { - "state": "ready", - "message": "Video indexing completed successfully.", - "summary": { - "index_schema_version": INDEX_SCHEMA_VERSION, - "dataset": "local", - "split": "local", - "run_id": "default", - "video_id": "video-1", - }, -} - - -def result_for(modality, timestamp): - return SearchResult( - query_id="query-1", - query="example", - modality=modality, - hits=( - SearchHit( - rank=1, - video_id="video-1", - start=timestamp, - end=timestamp + 1, - score=-0.1, - raw_distance=0.1, - modality=modality, - source_id=f"run:video-1:{modality}:1", - metadata={}, - ), - ), - ) - - -def frontend_harness(video_path, actor_output_path): - from pathlib import Path - from shutil import copyfile - from unittest.mock import Mock, patch - - from vidxp import frontend - from vidxp.capabilities.schemas import SearchHit, SearchResult - from vidxp.core.contracts import INDEX_SCHEMA_VERSION - - video_path = Path(video_path) - actor_output_path = Path(actor_output_path) - service = Mock() - service.check_dependencies.return_value = {"ok": True} - service.index_status.return_value = { - "state": "ready", - "message": "Video indexing completed successfully.", - "summary": { - "index_schema_version": INDEX_SCHEMA_VERSION, - "dataset": "local", - "split": "local", - "run_id": "default", - "video_id": "video-1", - }, - } - - def search_result(modality, timestamp): - return SearchResult( - query_id="q", - query="example", - modality=modality, - hits=( - SearchHit( - rank=1, - video_id="video-1", - start=timestamp, - end=timestamp + 1, - score=-0.1, - raw_distance=0.1, - modality=modality, - source_id=f"run:video-1:{modality}:1", - ), - ), +from vidxp.application_models import ( + ApplicationError, + DependencyCheckResult, + CreateActorOverlayCommand, + ErrorCategory, + FusedMoment, + FusedSearchResult, + FusionProvenance, + IndexStatus, + JobState, + SearchHit, +) +from vidxp.capabilities.registry import create_capability_registry +from vidxp.capability_service import CapabilityService +from vidxp.control_plane import ControlPlaneApplication +from vidxp.repository_layout import RepositoryLayout +from vidxp.settings import LocalExecutionSettings, VidXPSettings + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" + + +class UploadedVideo: + name = "video.mp4" + + def __init__(self, content: bytes): + self.content = content + + def getvalue(self): + return self.content + + +class FrontendTests(unittest.TestCase): + def test_model_download_sizes_are_human_readable(self): + self.assertEqual(frontend._format_bytes(1_539_458_338), "1.43 GiB") + self.assertEqual(frontend._format_bytes(38_926_091), "37.1 MiB") + + def test_durable_job_ids_restore_from_query_parameters(self): + session_state = {} + query_params = { + frontend.INDEX_JOB_QUERY_PARAM: "index-job", + frontend.SEARCH_JOB_QUERY_PARAM: "search-job", + frontend.SEARCH_TYPE_QUERY_PARAM: "scene", + } + with ( + patch.object(frontend.st, "session_state", session_state), + patch.object(frontend.st, "query_params", query_params), + ): + frontend._restore_durable_jobs() + + self.assertEqual( + session_state[frontend.INDEX_JOB_ID_KEY], + "index-job", ) + self.assertEqual( + session_state[frontend.SEARCH_RESULT_KEY], + { + "type": "scene", + "query": "", + "job_id": "search-job", + }, + ) + + def test_query_modalities_use_real_capability_service_contracts(self): + with TemporaryDirectory() as directory: + root = Path(directory) + service = ControlPlaneApplication( + layout=RepositoryLayout(root=root), + capabilities=CapabilityService( + create_capability_registry() + ), + media=Mock(), + artifacts=Mock(), + index_status=lambda: None, + model_cache=root / "models", + ) + with patch.object( + frontend, + "_configured_service", + return_value=service, + ): + available = frontend._available_query_modalities( + ("dialogue", "scene", "actor"), + ) - def search(modality, *_args, **_kwargs): - return search_result( - modality, - 17.25 if modality == "dialogue" else 42.5, + self.assertEqual( + available, + ("dialogue", "scene", "actor"), ) - def generate_actor_result(*_): - copyfile(video_path, actor_output_path) + def tearDown(self): + frontend._configured_service.cache_clear() + frontend._configured_jobs.cache_clear() + + def service(self, root: Path) -> Mock: + service = Mock() + service.layout.media = root / "media" + service.layout.artifacts = root / "artifacts" + service.layout.root = root + service.registry = create_capability_registry() + capabilities = CapabilityService(service.registry) + service.list_capabilities.side_effect = capabilities.list + service.get_capability.side_effect = capabilities.get + return service + + def test_service_is_composed_lazily_and_cached(self): + application = Mock() + with patch.object( + frontend, + "create_application", + return_value=application, + ) as create: + expected = VidXPSettings( + repository_root=Path("repository"), + runtime_backend="cpu", + model_cache=Path("model-cache"), + allow_model_downloads=False, + max_loaded_models=5, + max_concurrent_indexing=2, + max_concurrent_inference=3, + cpu_thread_budget=6, + external_capabilities=True, + capability_allowlist=("acme:ocr",), + ) + settings = frontend._settings_from_arguments( + ( + "--vidxp-settings-json", + LocalExecutionSettings.from_settings( + expected + ).model_dump_json(), + ) + ) + first = frontend._configured_service(settings) + second = frontend._configured_service(settings) - service.search.side_effect = search - service.render_actor.side_effect = generate_actor_result - with ( - patch.object(frontend, "SERVICE", service), - patch.object(frontend, "SAVED_VIDEO_PATH", video_path), - patch.object(frontend, "ACTOR_OUTPUT_PATH", actor_output_path), - patch.object(frontend, "indexing_in_progress", return_value=False), - ): - frontend.run() + self.assertIs(first, application) + self.assertIs(second, application) + create.assert_called_once() + self.assertEqual(settings, expected) + def test_media_identity_controls_search_readiness(self): + with TemporaryDirectory() as directory: + service = self.service(Path(directory)) + with patch.object( + frontend, + "_configured_service", + return_value=service, + ): + matching = { + "state": "ready", + "summary": {"media_ids": [MEDIA_ID]}, + } + stale = { + "state": "ready", + "summary": {"media_ids": []}, + } + self.assertTrue(frontend._is_search_ready(matching, MEDIA_ID)) + self.assertFalse(frontend._is_search_ready(stale, MEDIA_ID)) + + def test_single_registered_media_is_selected_before_indexing(self): + asset = SimpleNamespace(media_id=MEDIA_ID) + + self.assertEqual( + frontend._default_media_id(None, (asset,)), + MEDIA_ID, + ) + self.assertEqual( + frontend._default_media_id("missing", (asset,)), + MEDIA_ID, + ) -class FrontendSearchTests(unittest.TestCase): - def setUp(self): - self.service = Mock() - self.service.index_status.return_value = READY_STATUS + def test_multiple_registered_media_preserves_only_valid_selection(self): + assets = ( + SimpleNamespace(media_id=MEDIA_ID), + SimpleNamespace(media_id="323456781234423481234567890abcde"), + ) - def test_scene_search_uses_shared_service(self): - self.service.search.return_value = result_for("scene", 12.5) - with patch.object(frontend, "SERVICE", self.service): - result = frontend._run_search("scene", "yellow taxi") + self.assertEqual( + frontend._default_media_id(MEDIA_ID, assets), + MEDIA_ID, + ) + self.assertIsNone(frontend._default_media_id("missing", assets)) - self.assertEqual(result["timestamp"], 12.5) - self.assertEqual(result["hit"]["video_id"], "video-1") - self.service.search.assert_called_once_with( - "scene", - "yellow taxi", - top_k=1, + def test_busy_video_layout_keeps_controls_and_preview_stable(self): + service = Mock() + service.open_media_content.return_value = SimpleNamespace( + path=Path("video.mp4") ) + media_page = SimpleNamespace( + items=( + SimpleNamespace( + media_id=MEDIA_ID, + original_filename="video.mp4", + duration_seconds=27.2, + ), + ), + next_cursor=None, + ) + with ( + patch.object( + frontend, + "_configured_service", + return_value=service, + ), + patch.object(frontend.st, "session_state", {}), + patch.object(frontend.st, "subheader"), + patch.object( + frontend.st, + "selectbox", + return_value=MEDIA_ID, + ) as selectbox, + patch.object( + frontend.st, + "expander", + return_value=nullcontext(), + ), + patch.object(frontend.st, "caption"), + patch.object(frontend.st, "text_input", return_value=""), + patch.object(frontend.st, "button", return_value=False), + patch.object( + frontend.st, + "file_uploader", + return_value=None, + ) as uploader, + patch.object(frontend.st, "video") as video, + ): + uploaded, media_id = frontend._select_video( + True, + MEDIA_ID, + media_page, + ) - def test_dialogue_search_returns_a_renderable_result(self): - self.service.search.return_value = result_for("dialogue", 8) - with patch.object(frontend, "SERVICE", self.service): - result = frontend._run_search("dialogue", "fresh bread") + self.assertIsNone(uploaded) + self.assertEqual(media_id, MEDIA_ID) + self.assertTrue(selectbox.call_args.kwargs["disabled"]) + self.assertTrue(uploader.call_args.kwargs["disabled"]) + video.assert_called_once_with("video.mp4", width=560) - self.assertEqual(result["type"], "dialogue") - self.assertEqual(result["timestamp"], 8.0) + def test_local_path_import_uses_the_shared_application_command(self): + service = Mock() + service.import_media.return_value = SimpleNamespace(media_id=MEDIA_ID) - def test_actor_search_uses_shared_service(self): + result = frontend._import_local_video( + service, + " sample.mp4 ", + ) + + self.assertEqual(result.media_id, MEDIA_ID) + command = service.import_media.call_args.args[0] + self.assertEqual(command.path, Path("sample.mp4")) + + def test_available_modalities_use_application_dependency_commands(self): with TemporaryDirectory() as directory: - output_path = Path(directory) / "actor.mp4" + service = self.service(Path(directory)) - def generate_result(*_): - output_path.write_bytes(b"video") + def check(command): + return DependencyCheckResult( + ok=command.modalities != ("actor",), + modalities=command.modalities, + checks=(), + ) - self.service.render_actor.side_effect = generate_result - with ( - patch.object(frontend, "SERVICE", self.service), - patch.object(frontend, "ACTOR_OUTPUT_PATH", output_path), + service.check_dependencies.side_effect = check + with patch.object( + frontend, + "_configured_service", + return_value=service, ): - result = frontend._run_search("actor", "3") + available = frontend._available_index_modalities() - self.service.render_actor.assert_called_once() - self.assertEqual(result["type"], "actor") + self.assertEqual(available, ("dialogue", "scene")) + self.assertTrue( + all( + not call.args[0].include_runtime_checks + for call in service.check_dependencies.call_args_list + ) + ) + + def test_scene_detail_control_is_conditional_and_defaults_to_balanced(self): + with patch.object( + frontend.st, + "selectbox", + return_value=2.0, + ) as selectbox: + selected = frontend._scene_sample_fps_control( + ("dialogue", "scene"), + disabled=False, + ) - def test_search_error_is_returned_for_persistent_rendering(self): - self.service.index_status.side_effect = IndexNotReadyError( - "Index is not ready." + self.assertEqual(selected, 2.0) + self.assertEqual( + selectbox.call_args.args[:2], + ("Scene detail", (0.5, 1.0, 2.0)), ) - with patch.object(frontend, "SERVICE", self.service): - result = frontend._run_search("scene", "yellow taxi") + self.assertEqual(selectbox.call_args.kwargs["index"], 1) - self.assertEqual(result, {"error": "Index is not ready."}) + with patch.object(frontend.st, "selectbox") as selectbox: + selected = frontend._scene_sample_fps_control( + ("dialogue",), + disabled=False, + ) - def test_starting_a_new_index_clears_the_previous_result(self): - state = { - frontend.SEARCH_RESULT_KEY: { - "type": "scene", - "timestamp": 12.5, - }, - frontend.INDEX_ERROR_KEY: "old error", - } - with patch.object(frontend.st, "session_state", state): - frontend._request_indexing() + self.assertIsNone(selected) + selectbox.assert_not_called() - self.assertTrue(state[frontend.INDEX_REQUESTED_KEY]) - self.assertNotIn(frontend.SEARCH_RESULT_KEY, state) - self.assertNotIn(frontend.INDEX_ERROR_KEY, state) + def test_indexing_submits_selected_scene_sample_rate(self): + jobs = Mock() + jobs.submit_index.return_value = SimpleNamespace(job_id="job-1") + service = Mock() + session_state = {frontend.MEDIA_ID_KEY: MEDIA_ID} + with ( + patch.object( + frontend, + "_configured_service", + return_value=service, + ), + patch.object(frontend, "_configured_jobs", return_value=jobs), + patch.object(frontend.st, "session_state", session_state), + patch.object(frontend.st, "query_params", {}), + patch.object(frontend.st, "rerun"), + ): + frontend._run_indexing( + None, + {}, + ("scene",), + scene_sample_fps=2.0, + ) - def test_available_index_modalities_excludes_missing_extras(self): - def dependency_status(modalities): - return {"ok": modalities == ("scene",)} + command = jobs.submit_index.call_args.args[0] + self.assertEqual(command.scene_sample_fps, 2.0) + service.require_models.assert_called_once_with(("scene",)) - self.service.check_dependencies.side_effect = dependency_status - with patch.object(frontend, "SERVICE", self.service): - available = frontend._available_index_modalities() + def test_indexing_omits_scene_sample_rate_without_scene(self): + jobs = Mock() + jobs.submit_index.return_value = SimpleNamespace(job_id="job-1") + session_state = {frontend.MEDIA_ID_KEY: MEDIA_ID} + with ( + patch.object(frontend, "_configured_service", return_value=Mock()), + patch.object(frontend, "_configured_jobs", return_value=jobs), + patch.object(frontend.st, "session_state", session_state), + patch.object(frontend.st, "query_params", {}), + patch.object(frontend.st, "rerun"), + ): + frontend._run_indexing(None, {}, ("dialogue",)) - self.assertEqual(available, ("scene",)) + command = jobs.submit_index.call_args.args[0] + self.assertIsNone(command.scene_sample_fps) - def test_indexing_passes_selected_modalities_to_the_worker(self): - uploaded_video = Mock() - uploaded_video.name = "source.mp4" - uploaded_video.getvalue.return_value = b"video" + def test_search_queues_shared_command_without_loading_models(self): with TemporaryDirectory() as directory: - saved_video = Path(directory) / "source-video.mp4" + root = Path(directory) + service = self.service(root) + service.index_status.return_value = IndexStatus( + schema_version=1, + state="ready", + stage="complete", + message="ready", + ) + jobs = Mock() + jobs.submit_search.return_value = Mock(job_id="job-1") with ( - patch.object(frontend, "SAVED_VIDEO_PATH", saved_video), - patch.object(frontend, "start_indexing") as start, - patch.object(frontend.st, "rerun"), + patch.object( + frontend, + "_configured_service", + return_value=service, + ), + patch.object( + frontend, + "_configured_jobs", + return_value=jobs, + ), ): - frontend._run_indexing( - uploaded_video, - {}, - ("scene",), - ) + result = frontend._run_search("scene", "taxi") - start.assert_called_once_with( - str(saved_video), - "source.mp4", - frontend.SERVICE, - modalities=("scene",), + self.assertEqual(result["job_id"], "job-1") + command = jobs.submit_search.call_args.args[0] + self.assertEqual(command.modalities, ("scene",)) + self.assertEqual(command.query, "taxi") + service.search.assert_not_called() + + def test_actor_search_queues_without_blocking_ui(self): + with TemporaryDirectory() as directory: + service = self.service(Path(directory)) + service.index_status.return_value = IndexStatus( + schema_version=1, + state="ready", + stage="complete", + message="ready", + ) + jobs = Mock() + jobs.submit_actor_overlay.return_value = Mock(job_id="job-1") + with ( + patch.object( + frontend, + "_configured_service", + return_value=service, + ), + patch.object( + frontend, + "_configured_jobs", + return_value=jobs, + ), + ): + result = frontend._run_search("actor", "actor-1") + + self.assertEqual( + result, + { + "type": "actor", + "query": "actor-1", + "job_id": "job-1", + }, + ) + jobs.submit_actor_overlay.assert_called_once_with( + CreateActorOverlayCommand(cluster_id="actor-1") ) + jobs.wait.assert_not_called() + + @staticmethod + def _render_job(job, result=None): + jobs = Mock() + jobs.get.return_value = job + session_state = {} + query_params = { + frontend.SEARCH_JOB_QUERY_PARAM: "job-1", + frontend.SEARCH_TYPE_QUERY_PARAM: "actor", + } + + def fragment(*, run_every): + assert run_every == "1s" + return lambda function: function - def test_cancellation_request_uses_the_worker_token(self): - state = {} with ( - patch.object(frontend.st, "session_state", state), - patch.object(frontend, "cancel_indexing", return_value=True), + patch.object( + frontend, + "_configured_jobs", + return_value=jobs, + ), + patch.object(frontend.st, "fragment", side_effect=fragment), + patch.object(frontend.st, "session_state", session_state), + patch.object(frontend.st, "query_params", query_params), + patch.object(frontend.st, "rerun") as rerun, ): - frontend._request_cancellation() + frontend._render_search_result( + result or { + "type": "actor", + "query": "actor-1", + "job_id": "job-1", + } + ) + return session_state, rerun - self.assertTrue(state[frontend.CANCEL_REQUESTED_KEY]) + def test_terminal_index_failure_survives_browser_refresh(self): + session_state = { + frontend.INDEX_JOB_ID_KEY: "job-1", + } + query_params = { + frontend.INDEX_JOB_QUERY_PARAM: "job-1", + } + error = Mock(message="Indexing failed.") + job = Mock(state=JobState.failed, error=error) - def test_search_results_render_and_survive_widget_reruns(self): - with TemporaryDirectory() as directory: - video_path = Path(directory) / "video.mp4" - actor_output_path = Path(directory) / "actor.mp4" - video_path.write_bytes(b"video") - app = AppTest.from_function( - frontend_harness, - args=(str(video_path), str(actor_output_path)), - ).run() - - app.text_input[0].set_value("yellow taxi").run() - app.button[1].click().run() - self.assertEqual(list(app.exception), []) - self.assertEqual(len(app.get("video")), 2) - self.assertEqual( - app.session_state[frontend.SEARCH_RESULT_KEY]["type"], - "scene", - ) + with ( + patch.object(frontend.st, "session_state", session_state), + patch.object(frontend.st, "query_params", query_params), + ): + frontend._finish_index_job(job) + + self.assertNotIn(frontend.INDEX_JOB_ID_KEY, session_state) + self.assertNotIn(frontend.INDEX_JOB_QUERY_PARAM, query_params) + self.assertEqual( + session_state[frontend.INDEX_ERROR_KEY], + "Indexing failed.", + ) + + def test_actor_poll_persists_success_before_stopping(self): + job = Mock(state=JobState.succeeded, error=None) + job.result.result.artifact_id = "artifact-1" + + session_state, rerun = self._render_job(job) - app.selectbox[0].select("dialogue").run() - self.assertEqual(len(app.get("video")), 2) - self.assertEqual( - app.session_state[frontend.SEARCH_RESULT_KEY]["type"], - "scene", + self.assertEqual( + session_state[frontend.SEARCH_RESULT_KEY], + { + "type": "actor", + "query": "actor-1", + "artifact_id": "artifact-1", + }, + ) + rerun.assert_called_once_with() + + def test_queued_search_is_presented_as_starting(self): + job = Mock(state=JobState.queued, progress=None) + + with patch.object(frontend.st, "markdown") as markdown: + self._render_job( + job, + { + "type": "scene", + "query": "three people at the counter", + "job_id": "job-1", + }, ) - app.text_input[0].set_value("fresh bread").run() - app.button[1].click().run() - self.assertEqual( - app.session_state[frontend.SEARCH_RESULT_KEY]["type"], - "dialogue", + markdown.assert_called_once_with("⏳ Starting scene search...") + + def test_scene_result_describes_similarity_and_limitations(self): + service = Mock() + service.open_media_content.return_value = SimpleNamespace( + path=Path("video.mp4"), + ) + with ( + patch.object( + frontend, + "_configured_service", + return_value=service, + ), + patch.object(frontend.st, "success") as success, + patch.object(frontend.st, "caption") as caption, + patch.object(frontend.st, "video") as video, + ): + frontend._render_search_result( + { + "type": "scene", + "query": "three people at the counter", + "timestamp": 24.024, + "media_id": MEDIA_ID, + } ) + success.assert_called_once_with( + "Closest sampled scene: 24.0 seconds" + ) + caption.assert_called_once_with( + "Scene search ranks sampled frames by visual similarity. " + "It does not identify the first occurrence and is not reliable " + "for counting people." + ) + video.assert_called_once_with( + "video.mp4", + start_time=24.024, + width="stretch", + ) + + def test_actor_poll_persists_unavailable_job_error(self): + session_state, rerun = self._render_job(None) + + self.assertEqual( + session_state[frontend.SEARCH_RESULT_KEY], + { + "type": "actor", + "query": "actor-1", + "error": "The actor overlay job is unavailable.", + }, + ) + rerun.assert_called_once_with() + + def test_search_poll_keeps_durable_job_on_retryable_backend_failure(self): + jobs = Mock() + jobs.get.side_effect = ApplicationError( + "job_backend_unavailable", + ErrorCategory.unavailable, + "The durable job backend is unavailable.", + retryable=True, + ) + result = { + "type": "scene", + "query": "taxi", + "job_id": "job-1", + } + session_state = {frontend.SEARCH_RESULT_KEY: result} + + def fragment(*, run_every): + self.assertEqual(run_every, "1s") + return lambda function: function + + with ( + patch.object( + frontend, + "_configured_jobs", + return_value=jobs, + ), + patch.object(frontend.st, "fragment", side_effect=fragment), + patch.object(frontend.st, "session_state", session_state), + patch.object(frontend.st, "warning") as warning, + patch.object(frontend.st, "rerun") as rerun, + ): + frontend._render_search_result(result) + + self.assertEqual(session_state[frontend.SEARCH_RESULT_KEY], result) + warning.assert_called_once_with( + "The scene search status is temporarily unavailable. Retrying." + ) + rerun.assert_not_called() + + def test_job_lookup_only_suppresses_not_found(self): + jobs = Mock() + jobs.get.side_effect = ApplicationError( + "resource_not_found", + ErrorCategory.not_found, + "The requested job was not found.", + ) + self.assertIsNone(frontend._get_job(jobs, "job-1")) + + outage = ApplicationError( + "job_backend_unavailable", + ErrorCategory.unavailable, + "The durable job backend is unavailable.", + retryable=True, + ) + jobs.get.side_effect = outage + with self.assertRaises(ApplicationError) as raised: + frontend._get_job(jobs, "job-1") + self.assertIs(raised.exception, outage) + + def test_actor_poll_persists_failed_job_error(self): + error = Mock(message="Overlay rendering failed.") + job = Mock(state=JobState.failed, error=error, result=None) + + session_state, rerun = self._render_job(job) + + self.assertEqual( + session_state[frontend.SEARCH_RESULT_KEY], + { + "type": "actor", + "query": "actor-1", + "error": "Overlay rendering failed.", + }, + ) + rerun.assert_called_once_with() + + def test_actor_poll_persists_cancelled_job_error(self): + job = Mock(state=JobState.cancelled, error=None, result=None) + + session_state, rerun = self._render_job(job) + + self.assertEqual( + session_state[frontend.SEARCH_RESULT_KEY], + { + "type": "actor", + "query": "actor-1", + "error": "The actor overlay was cancelled.", + }, + ) + rerun.assert_called_once_with() + + def test_search_poll_persists_the_best_typed_hit(self): + hit = SearchHit( + rank=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=12.5, + end=13.0, + score=-0.1, + raw_distance=0.1, + modality="scene", + source_id="scene:1", + ) + completed = FusedSearchResult( + query_id="fused:taxi", + query="taxi", + modalities=("scene",), + moments=( + FusedMoment( + rank=1, + score=1 / 61, + media_id=MEDIA_ID, + start=12.5, + end=13.0, + modalities=("scene",), + hits=(hit,), + ), + ), + fusion=FusionProvenance( + requested_modalities=("scene",), + searched_modalities=("scene",), + ), + ) + job = Mock(state=JobState.succeeded, error=None) + job.result.result = completed + + session_state, rerun = self._render_job( + job, + { + "type": "scene", + "query": "taxi", + "job_id": "job-1", + }, + ) + + persisted = session_state[frontend.SEARCH_RESULT_KEY] + self.assertEqual(persisted["media_id"], MEDIA_ID) + self.assertEqual(persisted["timestamp"], 12.5) + rerun.assert_called_once_with() + + def test_restored_search_uses_completed_query_when_no_hit_exists(self): + completed = FusedSearchResult( + query_id="fused:taxi", + query="yellow taxi", + modalities=("scene",), + fusion=FusionProvenance( + requested_modalities=("scene",), + searched_modalities=("scene",), + ), + ) + job = Mock(state=JobState.succeeded, error=None) + job.result.result = completed + + session_state, rerun = self._render_job( + job, + { + "type": "scene", + "query": "", + "job_id": "job-1", + }, + ) + + self.assertEqual( + session_state[frontend.SEARCH_RESULT_KEY], + { + "type": "scene", + "query": "yellow taxi", + "error": "No scene match was found.", + }, + ) + rerun.assert_called_once_with() + if __name__ == "__main__": unittest.main() diff --git a/tests/test_frontend_app.py b/tests/test_frontend_app.py new file mode 100644 index 0000000..833d2fa --- /dev/null +++ b/tests/test_frontend_app.py @@ -0,0 +1,358 @@ +import unittest +from datetime import datetime, timezone +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace + +from streamlit.testing.v1 import AppTest + +from vidxp.application_models import ( + DependencyCheckResult, + FusedMoment, + FusedSearchResult, + FusionProvenance, + IndexStatus, + IndexStatusSummary, + Job, + JobKind, + JobProgress, + JobQueue, + JobState, + MediaAsset, + MediaPage, + SearchHit, + SearchJobResult, +) +from vidxp.capabilities.registry import create_capability_registry +from vidxp.capability_service import CapabilityService +from vidxp.core.media import MediaState, MediaStream +from vidxp.ports import LocalFileResource + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" +SNAPSHOT_ID = "323456781234423481234567890abcde" +INDEX_JOB_ID = "423456781234423481234567890abcde" +SEARCH_JOB_ID = "523456781234423481234567890abcde" + + +def render_frontend(service, jobs): + from unittest.mock import patch + + from vidxp import frontend + + with ( + patch.object( + frontend, + "_configured_service", + return_value=service, + ), + patch.object( + frontend, + "_configured_jobs", + return_value=jobs, + ), + ): + frontend.run() + + +class FrontendApplicationStub: + def __init__(self, root: Path, status: IndexStatus): + self.layout = SimpleNamespace(root=root) + self.model_cache = root / "models" + self._status = status + self._capabilities = CapabilityService( + create_capability_registry() + ) + self._media = MediaAsset( + media_id=MEDIA_ID, + video_id=MEDIA_ID, + original_filename="video.mp4", + sha256="1" * 64, + byte_size=10, + detected_mime_type="video/mp4", + container="mp4", + duration_seconds=27.2, + streams=( + MediaStream( + index=0, + kind="video", + codec="h264", + width=1, + height=1, + ), + ), + state=MediaState.ready, + created_at=datetime.now(timezone.utc), + ) + self._video_path = root / "video.mp4" + self._video_path.write_bytes(b"test video") + + def list_capabilities(self): + return self._capabilities.list() + + def get_capability(self, name): + return self._capabilities.get(name) + + def check_dependencies(self, command): + return DependencyCheckResult( + ok=True, + modalities=command.modalities, + checks=(), + ) + + def model_readiness(self, modalities): + return DependencyCheckResult( + ok=True, + modalities=modalities, + checks=(), + ) + + def index_status(self): + return self._status + + def list_media(self, command): + del command + return MediaPage(items=(self._media,), total=1) + + def open_media_content(self, media_id): + if media_id != MEDIA_ID: + raise AssertionError(f"unexpected media ID: {media_id}") + return LocalFileResource( + path=self._video_path, + filename=self._media.original_filename, + mime_type=self._media.detected_mime_type, + byte_size=self._media.byte_size, + etag=self._media.sha256, + ) + + +class FrontendJobStub: + def __init__(self): + self.jobs = {} + self.submitted_searches = [] + + def get(self, job_id): + return self.jobs.get(job_id) + + def submit_search(self, command): + self.submitted_searches.append(command) + job = Job( + job_id=SEARCH_JOB_ID, + kind=JobKind.search, + state=JobState.queued, + queue=JobQueue.cpu, + ) + self.jobs[job.job_id] = job + return job + + def cancel(self, job_id): + raise AssertionError(f"unexpected cancellation: {job_id}") + + +def ready_status() -> IndexStatus: + return IndexStatus( + schema_version=1, + state="ready", + stage="complete", + message="The video index is ready.", + summary=IndexStatusSummary( + index_schema_version=1, + snapshot_id=SNAPSHOT_ID, + media_count=1, + media_ids=(MEDIA_ID,), + modalities=("dialogue", "scene", "actor"), + ), + ) + + +def scene_search_result() -> FusedSearchResult: + hit = SearchHit( + rank=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=3.0, + end=4.0, + score=0.8, + raw_distance=0.2, + modality="scene", + source_id="scene:3", + ) + return FusedSearchResult( + query_id="query-1", + query="man in gray pants", + modalities=("scene",), + moments=( + FusedMoment( + rank=1, + score=0.8, + media_id=MEDIA_ID, + start=3.0, + end=4.0, + modalities=("scene",), + hits=(hit,), + ), + ), + fusion=FusionProvenance( + requested_modalities=("scene",), + searched_modalities=("scene",), + ), + ) + + +class FrontendAppTests(unittest.TestCase): + def setUp(self): + self.temporary_directory = TemporaryDirectory() + self.root = Path(self.temporary_directory.name) + + def tearDown(self): + self.temporary_directory.cleanup() + + @staticmethod + def widget(elements, label): + return next(item for item in elements if item.label == label) + + def app(self, service, jobs): + return AppTest.from_function( + render_frontend, + args=(service, jobs), + default_timeout=10, + ) + + def test_ready_page_renders_once_and_submits_first_search(self): + service = FrontendApplicationStub(self.root, ready_status()) + jobs = FrontendJobStub() + app = self.app(service, jobs).run() + + self.assertEqual(tuple(app.exception), ()) + self.assertEqual(len(app.get("video")), 1) + search_form = app.get("form") + self.assertEqual(len(search_form), 1) + self.assertTrue(search_form[0].proto.form.enter_to_submit) + search_type = self.widget(app.selectbox, "Search type") + search_query = app.text_input(key="video_search_query") + search_button = self.widget(app.button, "Search") + self.assertFalse(search_type.disabled) + self.assertFalse(search_query.disabled) + self.assertFalse(search_button.disabled) + + search_type.select("scene") + search_query.input("man in gray pants") + search_button.click() + app.run() + + self.assertEqual(tuple(app.exception), ()) + self.assertEqual(len(jobs.submitted_searches), 1) + self.assertEqual( + jobs.submitted_searches[0].query, + "man in gray pants", + ) + self.assertTrue( + any( + item.value == "⏳ Starting scene search..." + for item in app.markdown + ) + ) + + def test_ready_page_rejects_empty_search_without_disabling_form(self): + service = FrontendApplicationStub(self.root, ready_status()) + jobs = FrontendJobStub() + app = self.app(service, jobs).run() + + search_button = self.widget(app.button, "Search") + search_button.click() + app.run() + + self.assertEqual(tuple(app.exception), ()) + self.assertFalse(self.widget(app.button, "Search").disabled) + self.assertEqual(app.warning[-1].value, "Enter a search query.") + self.assertEqual(jobs.submitted_searches, []) + + def test_running_index_keeps_one_preview_and_disables_mutations(self): + service = FrontendApplicationStub(self.root, ready_status()) + jobs = FrontendJobStub() + jobs.jobs[INDEX_JOB_ID] = Job( + job_id=INDEX_JOB_ID, + kind=JobKind.index, + state=JobState.running, + queue=JobQueue.cpu, + progress=JobProgress( + stage="scene", + message="Indexing sampled scenes.", + current=2, + total=10, + updated_at=datetime.now(timezone.utc), + ), + ) + app = self.app(service, jobs) + app.query_params["index_job"] = INDEX_JOB_ID + app.run() + + self.assertEqual(tuple(app.exception), ()) + self.assertEqual(len(app.get("video")), 1) + self.assertTrue( + self.widget(app.selectbox, "Registered video").disabled + ) + self.assertTrue(app.file_uploader[0].disabled) + self.assertTrue(app.multiselect[0].disabled) + self.assertTrue(self.widget(app.selectbox, "Scene detail").disabled) + self.assertTrue(self.widget(app.selectbox, "Search type").disabled) + self.assertTrue( + app.text_input(key="video_search_query").disabled + ) + self.assertTrue(self.widget(app.button, "Index video").disabled) + self.assertTrue(self.widget(app.button, "Search").disabled) + self.assertFalse( + self.widget(app.button, "Cancel indexing").disabled + ) + self.assertTrue( + any( + item.value == "⏳ Indexing sampled scenes." + for item in app.markdown + ) + ) + + def test_restored_search_job_moves_from_queued_to_typed_result(self): + service = FrontendApplicationStub(self.root, ready_status()) + jobs = FrontendJobStub() + jobs.jobs[SEARCH_JOB_ID] = Job( + job_id=SEARCH_JOB_ID, + kind=JobKind.search, + state=JobState.queued, + queue=JobQueue.cpu, + ) + app = self.app(service, jobs) + app.query_params["search_job"] = SEARCH_JOB_ID + app.query_params["search_type"] = "scene" + app.run() + + self.assertEqual(tuple(app.exception), ()) + self.assertTrue( + any( + item.value == "⏳ Starting scene search..." + for item in app.markdown + ) + ) + + jobs.jobs[SEARCH_JOB_ID] = Job( + job_id=SEARCH_JOB_ID, + kind=JobKind.search, + state=JobState.succeeded, + queue=JobQueue.cpu, + result=SearchJobResult(result=scene_search_result()), + ) + app.run() + + self.assertEqual(tuple(app.exception), ()) + self.assertEqual(len(app.get("video")), 2) + self.assertEqual( + app.success[-1].value, + "Closest sampled scene: 3.0 seconds", + ) + self.assertNotIn("search_job", app.query_params) + self.assertNotIn("search_type", app.query_params) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_mcp.py b/tests/test_mcp.py new file mode 100644 index 0000000..eda1d88 --- /dev/null +++ b/tests/test_mcp.py @@ -0,0 +1,733 @@ +import asyncio +import base64 +import contextlib +import io +import json +import socket +import sys +import unittest +from datetime import datetime, timezone +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import Mock + +import httpx2 +import uvicorn +from fastapi.testclient import TestClient +from mcp.client import Client +from mcp.client.stdio import StdioServerParameters, stdio_client +from mcp.client.streamable_http import streamable_http_client +from mcp.shared.exceptions import MCPError +from mcp.types import ResourceLink + +from vidxp.application_models import ( + ApplicationError, + Artifact, + ErrorCategory, + IndexStatus, + Job, + JobKind, + JobPage, + JobQueue, + JobState, + MediaPage, + Principal, + QueryVideoCommand, +) +from vidxp.authentication import ( + AuthenticatedBearer, + OIDCBearerAuthenticator, + create_authenticator, +) +from vidxp.api import create_app +from vidxp.authorization import AuthorizationPolicy +from vidxp.branding import ( + ICON_MIME_TYPE, + ICON_SIZE, + PROJECT_URL, + icon_bytes, +) +from vidxp.composition import HttpApplicationContext +from vidxp.control_plane import ControlPlaneApplication +from vidxp.core.artifacts import ArtifactKind, ArtifactState +from vidxp.job_service import JobService +from vidxp.mcp import VidXPTokenVerifier, create_mcp_server +from vidxp.mcp_cli import main as mcp_main +from vidxp.mcp_cli import stdio_client_config +from vidxp.ports import LocalFileResource +from vidxp.settings import VidXPSettings + + +MEDIA_ID = "123456781234423481234567890abcde" +JOB_ID = "223456781234423481234567890abcde" +ARTIFACT_ID = "323456781234423481234567890abcde" + + +def queued_job() -> Job: + return Job( + job_id=JOB_ID, + kind=JobKind.index, + state=JobState.queued, + queue=JobQueue.cpu, + ) + + +class MCPTests(unittest.IsolatedAsyncioTestCase): + def context( + self, + root: Path, + *, + static_token: str | None = None, + http_trusted_hosts: tuple[str, ...] = ( + "127.0.0.1", + "testserver", + ), + mcp_allowed_hosts: tuple[str, ...] = ("127.0.0.1:*",), + mcp_allowed_origins: tuple[str, ...] = (), + ) -> HttpApplicationContext: + settings = VidXPSettings( + repository_root=root, + runtime_backend="cpu", + http_auth_mode="static" if static_token is not None else "none", + http_static_bearer_token=static_token, + http_trusted_hosts=http_trusted_hosts, + mcp_allowed_hosts=mcp_allowed_hosts, + mcp_allowed_origins=mcp_allowed_origins, + ) + application = Mock(spec=ControlPlaneApplication) + application.list_capabilities.return_value = () + application.index_status.return_value = IndexStatus( + schema_version=2, + state="missing", + stage="status", + message="No index.", + ) + jobs = Mock(spec=JobService) + readiness = Mock() + readiness.ready.return_value = True + return HttpApplicationContext( + application=application, + jobs=jobs, + readiness=readiness, + authenticator=create_authenticator(settings), + authorization=AuthorizationPolicy(), + settings=settings, + ) + + async def test_curated_tools_have_structured_output_schemas(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + server = create_mcp_server( + context, + default_principal=Principal( + subject="local", + scopes=frozenset({"*"}), + ), + ) + async with Client(server) as client: + discovered = await client.list_tools() + result = await client.call_tool("list_capabilities", {}) + + self.assertEqual( + [tool.name for tool in discovered.tools], + [ + "list_capabilities", + "get_capability", + "get_runtime_readiness", + "list_media", + "get_media", + "get_index_status", + "start_indexing", + "prepare_models", + "search_moments", + "query_video", + "create_clip", + "get_artifact_download", + "list_jobs", + "get_job", + "retry_job", + "cancel_job", + ], + ) + self.assertTrue( + all(tool.output_schema is not None for tool in discovered.tools) + ) + tools = {tool.name: tool for tool in discovered.tools} + for name in ("search_moments", "query_video"): + schema = tools[name].input_schema + command_ref = schema["properties"]["command"]["$ref"] + command_name = command_ref.rsplit("/", 1)[-1] + media_description = schema["$defs"][command_name]["properties"][ + "media_id" + ]["description"] + self.assertIn("omit it", media_description) + self.assertIn("active index snapshot", media_description) + self.assertEqual(result.structured_content, {"items": []}) + self.assertFalse(result.is_error) + + def test_stdio_help_and_config_are_ready_to_copy(self): + config = stdio_client_config( + command=r"C:\VidXP\vidxp-mcp.exe", + repository="library", + ) + self.assertEqual( + config, + { + "mcpServers": { + "vidxp": { + "command": r"C:\VidXP\vidxp-mcp.exe", + "args": ["--repository", "library"], + } + } + }, + ) + + output = io.StringIO() + with contextlib.redirect_stdout(output): + with self.assertRaises(SystemExit) as raised: + mcp_main(["--help"]) + self.assertEqual(raised.exception.code, 0) + self.assertIn("COPY/PASTE MCP CLIENT CONFIG", output.getvalue()) + self.assertIn('"mcpServers"', output.getvalue()) + + output = io.StringIO() + with contextlib.redirect_stdout(output): + mcp_main(["--print-config", "--repository", "library"]) + rendered = json.loads(output.getvalue()) + self.assertEqual( + rendered["mcpServers"]["vidxp"]["args"], + ["--repository", "library"], + ) + + def test_stdio_check_performs_handshake_and_tool_probe(self): + output = io.StringIO() + with TemporaryDirectory() as directory: + with contextlib.redirect_stdout(output): + mcp_main( + [ + "--check", + "--data-dir", + directory, + "--device", + "cpu", + ] + ) + + rendered = output.getvalue() + self.assertIn("OK VidXP MCP", rendered) + self.assertIn("Index state: missing", rendered) + self.assertIn("Tools: 16", rendered) + self.assertIn("get_index_status", rendered) + + async def test_server_info_exposes_vidxp_branding(self): + with TemporaryDirectory() as directory: + server = create_mcp_server( + self.context(Path(directory)), + default_principal=Principal( + subject="local", + scopes=frozenset({"*"}), + ), + ) + async with Client(server) as client: + server_info = client.server_info + + self.assertIsNotNone(server_info) + self.assertEqual(server_info.title, "VidXP") + self.assertEqual(server_info.website_url, PROJECT_URL) + self.assertEqual(len(server_info.icons or ()), 1) + icon = server_info.icons[0] + self.assertEqual(icon.mime_type, ICON_MIME_TYPE) + self.assertEqual(icon.sizes, [ICON_SIZE]) + prefix = f"data:{ICON_MIME_TYPE};base64," + self.assertTrue(icon.src.startswith(prefix)) + self.assertEqual( + base64.b64decode(icon.src.removeprefix(prefix)), + icon_bytes(), + ) + + async def test_index_submission_uses_shared_stable_idempotency(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.submit_index.return_value = queued_job() + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"vidxp.write"}), + ), + ) + arguments = { + "command": { + "media_id": MEDIA_ID, + "modalities": ["scene"], + "scene_sample_fps": 2.0, + }, + "idempotency_key": "agent-request-0001", + } + async with Client(server) as client: + first = await client.call_tool("start_indexing", arguments) + second = await client.call_tool("start_indexing", arguments) + + self.assertEqual( + first.structured_content, + second.structured_content, + ) + calls = context.jobs.submit_index.call_args_list + self.assertEqual(len(calls), 2) + self.assertEqual(calls[0].args[0].scene_sample_fps, 2.0) + self.assertEqual( + calls[0].kwargs["job_id"], + calls[1].kwargs["job_id"], + ) + self.assertEqual( + context.application.require_models.call_args.args[0], + ("scene",), + ) + + async def test_missing_models_fail_before_index_submission(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.require_models.side_effect = ApplicationError( + "model_unavailable", + ErrorCategory.unavailable, + "Run vidxp prepare --modalities scene.", + details={ + "capability": "scene", + "remediation": "vidxp prepare --modalities scene", + }, + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"vidxp.write"}), + ), + ) + async with Client(server) as client: + result = await client.call_tool( + "start_indexing", + { + "command": { + "media_id": MEDIA_ID, + "modalities": ["scene"], + }, + "idempotency_key": "agent-request-0002", + }, + ) + + self.assertTrue(result.is_error) + self.assertIn( + '"remediation":"vidxp prepare --modalities scene"', + result.content[0].text, + ) + context.jobs.submit_index.assert_not_called() + + async def test_query_video_submits_the_shared_durable_command(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.submit_query.return_value = queued_job().model_copy( + update={"kind": JobKind.query} + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"vidxp.read"}), + ), + ) + async with Client(server) as client: + result = await client.call_tool( + "query_video", + { + "command": { + "question": "What happens after the taxi arrives?", + "media_id": MEDIA_ID, + "modalities": ["scene", "dialogue"], + }, + "idempotency_key": "agent-query-0001", + }, + ) + + self.assertFalse(result.is_error) + command = context.jobs.submit_query.call_args.args[0] + self.assertEqual( + command, + QueryVideoCommand( + question="What happens after the taxi arrives?", + media_id=MEDIA_ID, + modalities=("scene", "dialogue"), + ), + ) + self.assertIn( + "job_id", + context.jobs.submit_query.call_args.kwargs, + ) + + async def test_discovery_tools_use_shared_media_and_job_pages(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.list_media.return_value = MediaPage(total=0) + context.jobs.list.return_value = JobPage() + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"vidxp.read"}), + ), + ) + async with Client(server) as client: + media = await client.call_tool( + "list_media", + {"page_size": 7}, + ) + jobs = await client.call_tool( + "list_jobs", + {"page_size": 9}, + ) + + self.assertFalse(media.is_error) + self.assertFalse(jobs.is_error) + self.assertEqual( + media.structured_content, + {"items": [], "total": 0, "next_cursor": None}, + ) + self.assertEqual( + jobs.structured_content, + {"items": [], "next_cursor": None}, + ) + self.assertEqual( + context.application.list_media.call_args.args[0].page_size, + 7, + ) + self.assertEqual(context.jobs.list.call_args.args[0].page_size, 9) + + async def test_retry_job_uses_shared_stable_idempotency(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.jobs.retry.return_value = queued_job() + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"vidxp.write"}), + ), + ) + arguments = { + "job_id": JOB_ID, + "idempotency_key": "agent-retry-0001", + } + async with Client(server) as client: + first = await client.call_tool("retry_job", arguments) + second = await client.call_tool("retry_job", arguments) + + self.assertFalse(first.is_error) + self.assertEqual( + first.structured_content, + second.structured_content, + ) + calls = context.jobs.retry.call_args_list + self.assertEqual(len(calls), 2) + self.assertEqual( + calls[0].kwargs["retry_id"], + calls[1].kwargs["retry_id"], + ) + + async def test_clip_submission_and_lazy_artifact_download(self): + with TemporaryDirectory() as directory: + root = Path(directory) + clip = root / "clip.mp4" + clip.write_bytes(b"clip-content") + context = self.context(root) + context.jobs.submit_snippet.return_value = ( + queued_job().model_copy(update={"kind": JobKind.snippet}) + ) + context.application.get_artifact.return_value = Artifact( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="compatible_mp4", + mime_type="video/mp4", + byte_size=12, + sha256="1" * 64, + state=ArtifactState.ready, + created_at=datetime.now(timezone.utc), + ) + context.application.open_artifact_content.return_value = ( + LocalFileResource( + path=clip, + filename=f"snippet-{ARTIFACT_ID}.mp4", + mime_type="video/mp4", + byte_size=12, + etag="1" * 64, + ) + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"*"}), + ), + ) + async with Client(server) as client: + submitted = await client.call_tool( + "create_clip", + { + "command": { + "media_id": MEDIA_ID, + "start_seconds": 9, + "end_seconds": 17, + }, + "idempotency_key": "agent-clip-0001", + }, + ) + linked = await client.call_tool( + "get_artifact_download", + {"artifact_id": ARTIFACT_ID}, + ) + link = linked.content[0] + downloaded = await client.read_resource(str(link.uri)) + + self.assertFalse(submitted.is_error) + submitted_command = context.jobs.submit_snippet.call_args.args[0] + self.assertEqual(submitted_command.media_id, MEDIA_ID) + self.assertEqual(submitted_command.start_seconds, 9) + self.assertEqual(submitted_command.end_seconds, 17) + self.assertIsInstance(link, ResourceLink) + self.assertEqual(link.mime_type, "video/mp4") + self.assertEqual(link.size, 12) + self.assertEqual(downloaded.contents[0].blob, "Y2xpcC1jb250ZW50") + + async def test_application_errors_are_machine_readable_and_safe(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.index_status.side_effect = ApplicationError( + "index_busy", + ErrorCategory.conflict, + "The index is busy.", + retryable=True, + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"vidxp.read"}), + ), + ) + async with Client(server) as client: + result = await client.call_tool("get_index_status", {}) + + self.assertTrue(result.is_error) + error_text = result.content[0].text + self.assertIn('"code":"index_busy"', error_text) + self.assertIn('"protocol_code":-32009', error_text) + self.assertIn('"retryable":true', error_text) + + async def test_unexpected_errors_do_not_leak_exception_text(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.index_status.side_effect = RuntimeError( + "database-password" + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"vidxp.read"}), + ), + ) + async with Client(server) as client: + with self.assertRaises(MCPError) as caught: + await client.call_tool("get_index_status", {}) + + self.assertEqual(caught.exception.code, -32603) + self.assertNotIn("database-password", caught.exception.message) + self.assertEqual(caught.exception.data["code"], "internal_error") + + async def test_write_tools_enforce_repository_scope(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + server = create_mcp_server( + context, + default_principal=Principal( + subject="reader", + scopes=frozenset({"vidxp.read"}), + ), + ) + async with Client(server) as client: + result = await client.call_tool( + "start_indexing", + { + "command": { + "media_id": MEDIA_ID, + "modalities": ["scene"], + }, + "idempotency_key": "agent-request-0001", + }, + ) + + self.assertTrue(result.is_error) + self.assertIn('"protocol_code":-32003', result.content[0].text) + self.assertIn('"required_scope":"vidxp.write"', result.content[0].text) + context.jobs.submit_index.assert_not_called() + + async def test_stdio_entrypoint_serves_the_same_curated_surface(self): + with TemporaryDirectory() as directory: + parameters = StdioServerParameters( + command=sys.executable, + args=[ + "-m", + "vidxp.mcp_cli", + "--index-directory", + directory, + "--device", + "cpu", + ], + cwd=Path(__file__).parents[1], + ) + async with Client(stdio_client(parameters)) as client: + discovered = await client.list_tools() + result = await client.call_tool("list_capabilities", {}) + + self.assertEqual( + [tool.name for tool in discovered.tools], + [ + "list_capabilities", + "get_capability", + "get_runtime_readiness", + "list_media", + "get_media", + "get_index_status", + "start_indexing", + "prepare_models", + "search_moments", + "query_video", + "create_clip", + "get_artifact_download", + "list_jobs", + "get_job", + "retry_job", + "cancel_job", + ], + ) + self.assertEqual( + [item["name"] for item in result.structured_content["items"]], + ["dialogue", "scene", "actor"], + ) + + async def test_streamable_http_works_with_the_official_remote_client(self): + token = "s" * 32 + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + port = listener.getsockname()[1] + listener.close() + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + static_token=token, + ) + server = uvicorn.Server( + uvicorn.Config( + create_app(context=context), + host="127.0.0.1", + port=port, + log_level="critical", + ) + ) + serving = asyncio.create_task(server.serve()) + try: + for _attempt in range(200): + if server.started: + break + if serving.done(): + await serving + await asyncio.sleep(0.01) + else: + self.fail("The MCP HTTP fixture did not start.") + async with httpx2.AsyncClient( + headers={"Authorization": f"Bearer {token}"} + ) as http_client: + transport = streamable_http_client( + f"http://127.0.0.1:{port}/mcp", + http_client=http_client, + ) + async with Client(transport) as client: + discovered = await client.list_tools() + result = await client.call_tool( + "list_capabilities", + {}, + ) + finally: + server.should_exit = True + await serving + + self.assertEqual(len(discovered.tools), 16) + self.assertEqual(result.structured_content, {"items": []}) + + async def test_oidc_verifier_projects_the_shared_validated_token(self): + authenticator = Mock(spec=OIDCBearerAuthenticator) + authenticator.authenticate_bearer.return_value = AuthenticatedBearer( + principal=Principal( + subject="user-1", + client_id="client-1", + scopes=frozenset({"vidxp.read"}), + ), + expires_at=1_800_000_000, + resource="https://api.example/mcp", + claims={"iss": "https://issuer.example"}, + ) + + token = await VidXPTokenVerifier(authenticator).verify_token("token") + + self.assertEqual(token.subject, "user-1") + self.assertEqual(token.client_id, "client-1") + self.assertEqual(token.scopes, ["vidxp.read"]) + self.assertEqual(token.expires_at, 1_800_000_000) + self.assertEqual(token.resource, "https://api.example/mcp") + + def test_sdk_transport_security_rejects_host_and_origin(self): + token = "s" * 32 + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + static_token=token, + http_trusted_hosts=("*",), + mcp_allowed_hosts=("mcp.example",), + mcp_allowed_origins=("https://client.example",), + ) + with TestClient(create_app(context=context)) as client: + bad_host = client.post( + "/mcp", + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Host": "other.example", + }, + json={}, + ) + bad_origin = client.post( + "/mcp", + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Host": "mcp.example", + "Origin": "https://other.example", + }, + json={}, + ) + unauthenticated_bad_origin = client.post( + "/mcp", + headers={ + "Content-Type": "application/json", + "Host": "mcp.example", + "Origin": "https://other.example", + }, + json={}, + ) + + self.assertEqual(bad_host.status_code, 421) + self.assertEqual(bad_origin.status_code, 403) + self.assertEqual(unauthenticated_bad_origin.status_code, 403) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_public_path_contracts.py b/tests/test_public_path_contracts.py new file mode 100644 index 0000000..6623473 --- /dev/null +++ b/tests/test_public_path_contracts.py @@ -0,0 +1,197 @@ +import json +import unittest +from dataclasses import is_dataclass +from pathlib import Path + +from pydantic import BaseModel, ValidationError + +from vidxp.application_models import ( + Artifact, + CreateActorOverlayCommand, + CreateIndexCommand, + CreateSnippetCommand, + ImportMediaCommand, + IndexResult, + IndexStatus, + MediaAsset, + MediaPage, + PrepareModelsResult, +) +from vidxp.capabilities.registry import create_capability_registry +from vidxp.capabilities.contracts import OperationDefinition +from vidxp.capabilities.schemas import SearchHit +from vidxp.ports import LocalFileResource + + +MEDIA_ID = "123456781234423481234567890abcde" +GENERATION_ID = "223456781234423481234567890abcde" + + +def schema_fields(schema): + fields = set() + if isinstance(schema, dict): + fields.update(schema.get("properties", ())) + for value in schema.values(): + fields.update(schema_fields(value)) + elif isinstance(schema, list): + for value in schema: + fields.update(schema_fields(value)) + return fields + + +class PublicPathContractTests(unittest.TestCase): + def test_shared_contracts_do_not_expose_paths_or_storage_keys(self): + models = [ + Artifact, + CreateActorOverlayCommand, + CreateIndexCommand, + CreateSnippetCommand, + IndexResult, + IndexStatus, + MediaAsset, + MediaPage, + PrepareModelsResult, + SearchHit, + ] + registry = create_capability_registry() + for definition in registry.definitions.values(): + for operation in definition.operations.values(): + models.extend((operation.input_model, operation.output_model)) + + forbidden = { + "path", + "input_path", + "output_path", + "storage_key", + "repository_root", + "index_directory", + } + for model in models: + with self.subTest(model=model.__name__): + schema = model.model_json_schema() + self.assertTrue(forbidden.isdisjoint(schema_fields(schema))) + self.assertNotIn('"format": "path"', json.dumps(schema)) + + def test_only_local_import_accepts_a_path(self): + command = ImportMediaCommand(path=Path("video.mp4")) + self.assertEqual(command.path, Path("video.mp4")) + with self.assertRaises(ValidationError): + CreateIndexCommand.model_validate( + { + "path": "video.mp4", + "media_id": MEDIA_ID, + "modalities": ["scene"], + } + ) + + def test_media_and_artifact_ids_are_not_content_hashes_or_names(self): + with self.assertRaises(ValidationError): + CreateIndexCommand(media_id="episode-1", modalities=("scene",)) + with self.assertRaises(ValidationError): + CreateIndexCommand(media_id="1" * 64, modalities=("scene",)) + + def test_scene_sampling_requires_a_positive_scene_request(self): + with self.assertRaises(ValidationError): + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("dialogue",), + scene_sample_fps=1.0, + ) + with self.assertRaises(ValidationError): + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + scene_sample_fps=0, + ) + + def test_scene_sampling_has_one_canonical_command_field(self): + nested = CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + capability_options={ + "scene": {"sample_fps": 2.0, "batch_size": 4}, + }, + ) + explicit = CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + scene_sample_fps=2.0, + capability_options={"scene": {"batch_size": 4}}, + ) + + self.assertEqual(nested, explicit) + self.assertEqual(nested.scene_sample_fps, 2.0) + self.assertEqual( + nested.capability_options, + {"scene": {"batch_size": 4}}, + ) + self.assertNotIn( + "sample_fps", + nested.model_dump(mode="json")["capability_options"]["scene"], + ) + with self.assertRaisesRegex(ValidationError, "conflicts"): + CreateIndexCommand( + media_id=MEDIA_ID, + modalities=("scene",), + scene_sample_fps=1.0, + capability_options={"scene": {"sample_fps": 2.0}}, + ) + + def test_search_metadata_rejects_internal_locations(self): + with self.assertRaises(ValidationError): + SearchHit( + rank=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=1, + end=2, + score=0, + raw_distance=0, + modality="scene", + source_id="source", + metadata={"source_path": "C:/secret/video.mp4"}, + ) + + with self.assertRaises(ValidationError): + SearchHit( + rank=1, + media_id=MEDIA_ID, + video_id=MEDIA_ID, + generation_id=GENERATION_ID, + start=1, + end=2, + score=0, + raw_distance=0, + modality="scene", + source_id="source", + metadata={"source": {"storage_key": "secret"}}, + ) + + def test_external_public_operation_cannot_expose_paths(self): + class UnsafeInput(BaseModel): + path: Path + + class SafeOutput(BaseModel): + ok: bool + + with self.assertRaises(ValidationError): + OperationDefinition( + input_model=UnsafeInput, + output_model=SafeOutput, + ) + + def test_local_file_handle_is_not_dataclass_serializable(self): + handle = LocalFileResource( + path=Path("internal"), + filename="video.mp4", + mime_type="video/mp4", + byte_size=1, + etag="1" * 64, + ) + self.assertFalse(is_dataclass(handle)) + self.assertFalse(hasattr(handle, "__dict__")) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..57ecd7e --- /dev/null +++ b/uv.lock @@ -0,0 +1,4668 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <3.15" +resolution-markers = [ + "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", + "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", + "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')", + "(python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')", + "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, +] + +[[package]] +name = "altair" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/a1/5e6cc638a66da48cfc89a79c2f4810dfec00b63385f9b009ab1f069779bb/altair-6.2.2.tar.gz", hash = "sha256:a1ff9d9cfe81c75414641826312b9471780e19d39293ba0b012933f6b6cba0fe", size = 766606, upload-time = "2026-06-23T12:47:13.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/99/d6031f4f146298951c46b1bf1cc160c2a63f6e44b3c13a30054add100d5f/altair-6.2.2-py3-none-any.whl", hash = "sha256:94014f8ad8617c3cb163d1137359cd6db5ba134b9b46d93cfd8b609fd245a583", size = 797613, upload-time = "2026-06-23T12:47:11.451Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "asgi-correlation-id" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/25/df29145ef02ca69548e82b50ed15ac5c438ab5caf7b8e717d2b1258f5bea/asgi_correlation_id-5.0.1.tar.gz", hash = "sha256:ad54374a300f000b8bef3b39738768842394de0a3c528a6f77ef3ca053eb89a1", size = 10642, upload-time = "2026-06-09T15:09:49.112Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/fe/3f5a304ddc2b625a6a2fda913b29bc4675c434016557592791cf1aa39a60/asgi_correlation_id-5.0.1-py3-none-any.whl", hash = "sha256:dfea97c1fc41e8eadc3e518a311427cba6e7e79d9ddb6a6c7ea8879f2408d91d", size = 12026, upload-time = "2026-06-09T15:09:48.155Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "av" +version = "18.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/a4/570a5a35c8638aba01e739925846c35fdd6b0756a15526766d0a4dd3b7df/av-18.0.0.tar.gz", hash = "sha256:4ef7e72c3d3a872584a1215173b16e0226811037f40dcdbf75992631098df1ba", size = 4340222, upload-time = "2026-07-02T06:37:58.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/4a/9e3463df030e063d757fa12f0f39be6541b45b06b5bad48c2ce361b924bf/av-18.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:149289d40e732a6e49c9530bc245b49d9964cfd1c8c9e06778703b7d5bba6b25", size = 22499354, upload-time = "2026-07-02T06:36:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/77/b3/2576a44b4f39c7462ced4c17fec04c756f7b0f3c5cb940d124173e417d6a/av-18.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:35274c20d2ad3b4774fe632bcef2e34af79858ddf899352339cc3babbc13a484", size = 18175248, upload-time = "2026-07-02T06:37:01.741Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/6732f17b96dc23fd23b876b2805435855abdc8a3b397142be4e581165de8/av-18.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4d683b7747a0ba9222b8a5f81e41db5f796e7f64473454ec4fe2548e083c2fa0", size = 33387843, upload-time = "2026-07-02T06:37:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/7708c43fed7ae28b4a1bad060b4221e3334cd827cec24f7165902a6ac1f4/av-18.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ae56b40b6f8b067a8ad2dac664fbfbabac7f7a55b9a7bb031eb99289252bc017", size = 35536910, upload-time = "2026-07-02T06:37:08.806Z" }, + { url = "https://files.pythonhosted.org/packages/5a/94/eba99691d184f6a395a242d54dc370e2fd2265e95bbc98e2963a0fdbdd6c/av-18.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ea2e8ebbce521f21b55df9400e00d721623c9020ef158f5a188a96130be0743f", size = 38984619, upload-time = "2026-07-02T06:37:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/0d7aee07fe16aa9ffdf96043c14bed5485a52c0dea4259de87aa306ecab4/av-18.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef96dabb3e50dac249913145dff5424b302b257fd95dcb64be3c7b7a8aef16d1", size = 34451176, upload-time = "2026-07-02T06:37:15.154Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/810da80b12680d4c4fe235bd1b4003289be9213ac7f114b77b8ecf0e3b3e/av-18.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0f65518a184613e41536f29e8758c8e3d8293e46bf5bef108f04f925bbfa3f44", size = 36619869, upload-time = "2026-07-02T06:37:18.495Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/0f121ff43dc5a70696676c98a8f1674e2fa787614c2abaacb15fa1a9bc99/av-18.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:aaf4d354d2beaa6651e4f92e54409a578bde64f79c0beef9a30b388d06f7c629", size = 27556236, upload-time = "2026-07-02T06:37:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f6/2509754d4d2356abc6fc0ea3d57c12ade29bac23a1fb7fc215a53ca518fb/av-18.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:adac2b3833b6cb9bd6cb52664a522b94db453615b3675b1dbb26e13fe1c80da6", size = 20221133, upload-time = "2026-07-02T06:37:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/4ee23a7f1609adf9b2f140c7a8ffade64a1449d89ab431d922a809eebf19/av-18.0.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:88dd8e35e9242662b409a6a05fd24a6775d949eb05da0ba31cab4f250eacbab5", size = 22740741, upload-time = "2026-07-02T06:37:26.659Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f0/b9f8363d07aa4521913e483f6a30c7c164973ef01de62769bf9b97049cd8/av-18.0.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f8f454349c402e2c8d6fa80b54eb2a3f86c00f414d2b399f01ae6dab075c6fd8", size = 18384189, upload-time = "2026-07-02T06:37:29.518Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e5/69397019aed280a72a43e97a252dee4295df1a9e608848452e5300ec4dab/av-18.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:88ce194c2201c6a6d40336adee8a5ddde46ed743eacb500e3ae9368d1c6d889e", size = 36749881, upload-time = "2026-07-02T06:37:33.096Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/1614d74f0d676ea6745eb59553c9ad01ca25db523cba808d522e838f4f5b/av-18.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:aa15e567a018cc94a26b0ab45da676dee70c4146ace6e92e47d30cc9689cbfbe", size = 38645927, upload-time = "2026-07-02T06:37:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3c/5f54710d69b0ea93634134f92b49c7a2a7fd27da5486a8a7e6251ac1cfb4/av-18.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:613153e48cefc91700746dde0ad0282d4677b194cba22cc771de14c78411cf8b", size = 40454783, upload-time = "2026-07-02T06:37:40.904Z" }, + { url = "https://files.pythonhosted.org/packages/26/92/8293e6a267e0591b543abd96ae01e7e8ed228509bdb4e4644a8a8395d90f/av-18.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:30404f53ca1ea7f350ac86ff22a2c04f903014758e9b33f398c5a62de34bd84f", size = 37573117, upload-time = "2026-07-02T06:37:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/0c/38ed7601277ae57dfe857d040be4762530fd728efff45c2fb8f035fef96a/av-18.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6882a48f7aec2863c96cddee3256ff2da98f7fb6cbed83cee9d7e70a8f186a6b", size = 39669026, upload-time = "2026-07-02T06:37:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/0636ca04d5d89d01c49bd366d2b660cc85d1f8117c476b2be62eb0c70855/av-18.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:55a646e9afce9fdc5de5224205a8a12c7ed1ba9803145dcc876c40bfc03a109b", size = 28448336, upload-time = "2026-07-02T06:37:52.477Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/1e24450ea981c44ed328691496fd2774dfa9fa3c3b00fd07f72fd5614abe/av-18.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:96f594ff506a09475e5549359352332049a25d37a08f00b4623f7f6e92e45b9c", size = 21377289, upload-time = "2026-07-02T06:37:55.935Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "chromadb" +version = "1.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/d1/5e33b26985f0c7046a0be1cee2158ada1748ee700d2545057fde1468d74d/chromadb-1.5.9.tar.gz", hash = "sha256:5c20e62a455c28bacac927f26116a73fd8e1799e0d908be8e8a4f02197a54731", size = 2595635, upload-time = "2026-05-05T05:54:51.713Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/5b/3cced915244f43ed14b53fe9f63a37f05f865064f4e4fe7d9448d3f2a352/chromadb-1.5.9-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60701011b5e6409647fa40d12c7c5a66b2b0bfcf33a52db2ad53a30a2abc4957", size = 22564540, upload-time = "2026-05-05T05:54:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/34/4c/adcef1f4e82a2ef69ccd3711d55fc289193d54c4c0ff7a0292a3631db46f/chromadb-1.5.9-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:814b9c95617377f6501e5757d63dfddb554a283a7739c87b9fa573850174e6f3", size = 21699698, upload-time = "2026-05-05T05:54:45.078Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/937bc4d2e6f8ab9664ec79931fbbd69efff47e513ec2924b071e4b0ff774/chromadb-1.5.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9192d111bd662241625867962333d99369a00769a50f8b2f58cb388731274d7e", size = 22680924, upload-time = "2026-05-05T05:54:36.25Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ec/0c42039e80b9acc534f67b73b7a42471948042859b3a64867b50a4a77fa3/chromadb-1.5.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc09b3df76e5a5cb386aed2715a2eea152e3949f9e1ba93c7119505377749929", size = 23316203, upload-time = "2026-05-05T05:54:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ce/0f7be6e5d0feafa2cda54b12e6542afeea7dea89d2d411e14da90f8abb96/chromadb-1.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:4fd0b560e56761b7f3cb4d5c6205fd5f20814484b4a3e4e9af9038c2b428fc6c", size = 23542454, upload-time = "2026-05-05T05:54:54.942Z" }, +] + +[[package]] +name = "chromadb-client" +version = "1.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "jsonschema" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/3b/47c02df4d56883f1dee3548d03a52e75ab1d1c282030ffaf32031bb46569/chromadb_client-1.5.9.tar.gz", hash = "sha256:5e481d51ce6634fe54230b0411bf1ff257607ace222fc64c669e27874c12175b", size = 22112301, upload-time = "2026-05-05T05:36:19.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/0c/4ae7985586638bcaf5459f6cad5e81704317df1946419c8a16fb8f9c1fbc/chromadb_client-1.5.9-py3-none-any.whl", hash = "sha256:3200b9f79389974f9a35bbcdd0657289f5352da3a7d0d37c63194283e02d0605", size = 798976, upload-time = "2026-05-05T05:36:15.966Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +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 = "ctranslate2" +version = "4.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pyyaml" }, + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c4/0e450796f90e54f3325697fc67db4f4ecd397aef96d7b3924e26fb8bd04b/ctranslate2-4.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c2db633a06e3b34bbfb72fd26eee58053d9df1f9c1610ac4df3a6a1e25af7d7", size = 1270559, upload-time = "2026-07-03T12:39:01.154Z" }, + { url = "https://files.pythonhosted.org/packages/b7/54/7b6db16470d0788fb8ab43a99e3e18ba9d41a9b50b7fef7dec353eafbe20/ctranslate2-4.8.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:079976cbce3a68de04bf9948d08c96beb86df44e5cd2974e4187bc9c9bb388f3", size = 11928069, upload-time = "2026-07-03T12:39:02.6Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/8fee1366631d224bf26b34db9063a0c88ce358d58331c2393689b0ea27ff/ctranslate2-4.8.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74bae0a8dc9f98c5a6100bf1c17a91782b384ea53b83e2606030ebf9f25318fe", size = 16707971, upload-time = "2026-07-03T12:39:05.09Z" }, + { url = "https://files.pythonhosted.org/packages/30/84/f610e90bb419707632b9b668476b9fd4cdb090c9b53c119ce017699b58ca/ctranslate2-4.8.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0a584c17f21779eb9035bcbc1ec280998f90b36725b70a5ff911f33e343199a", size = 39351971, upload-time = "2026-07-03T12:39:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/76/6c/7230ecbdd23ab867715e1b6ffe99211c39c11cae8ec2d6c3ec9208c38ee2/ctranslate2-4.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:82982f07a7d615d2248d17d6ec4c43cd50e534b094aa27cda62125a5e3a6e3fc", size = 19219248, upload-time = "2026-07-03T12:39:11.329Z" }, + { url = "https://files.pythonhosted.org/packages/6d/09/9a50eeab00db68aeac08f6ab7f98b5c36abd26b89cbd707ea39e70656500/ctranslate2-4.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9de0dddd91ae68da0a7323441e90708d14b31d31cd443004dda0e1198b5bf11e", size = 1270522, upload-time = "2026-07-03T12:39:13.368Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/6c41c4d3ae539ec76b1943c362184677befd7c1d5290d2ec361182cdb1e0/ctranslate2-4.8.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:82e0e6eb7d4301fd79a714495c8faf34242e09542cef04c9e9794c3fe90014a1", size = 11930367, upload-time = "2026-07-03T12:39:14.896Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d4/03428106134a0a58922461074f8942f92c5ed0bb3a8d018677ad64a9c476/ctranslate2-4.8.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ca144b93035b9f53e6d67b7cdf5802c3fffca9aa0247940eecbd4592c68ce2f", size = 16882768, upload-time = "2026-07-03T12:39:17.425Z" }, + { url = "https://files.pythonhosted.org/packages/47/c9/976a565398a03fb2973cbe5edd5ca03c4332d86b634799e0ee562420d3bc/ctranslate2-4.8.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dacc408f716ebc73b3b3c6ddd937700e776c4c68b6d9c81862990150ff0f6af6", size = 39529060, upload-time = "2026-07-03T12:39:20.468Z" }, + { url = "https://files.pythonhosted.org/packages/c0/82/0a5f7f2b03b4e10aacb3146715724e1b96bb993cc7d199be28c9825aa120/ctranslate2-4.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:49f96e861b57301f0b76a082109bde2cac8204a6b4fedc870883008271e82251", size = 19220789, upload-time = "2026-07-03T12:39:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/3101c3a0785253a8ef386f39744ad19c28c75b7f227e7c232aee7a5c416a/ctranslate2-4.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba628835e6ad4ad399261ab6cb51bf152de563e6b122a9e8eb0c61e69f925931", size = 1270478, upload-time = "2026-07-03T12:39:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/89/b9/e50c7558e96a054d6b1e6a6c5e729dda4a4f05584e065f2902aa5f1bc4c8/ctranslate2-4.8.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:85ef15ce0b2172ec471975b8a30d5c5bc71e7cffcd163ad6c07ea32f1943d940", size = 11930241, upload-time = "2026-07-03T12:39:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2f/ea7a19c6d7e949b731fb034664633184bbfc7882846d107f4d790693fb76/ctranslate2-4.8.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0030670278a73cae09dff9bca72cdd248af61f9367257f18db9b3b94fbb3a50d", size = 16883512, upload-time = "2026-07-03T12:39:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/99/4a/21f325a9d0925d8ad24b04249adf29bf9909442967603634f7f6d4acbb79/ctranslate2-4.8.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4242a7f8e285f922525f4cffd5b1fb43cbacc61d0611cf54832e9c447d030840", size = 39529085, upload-time = "2026-07-03T12:39:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/37da1a7500b57496a5269318c4f57962ea0c26dcac06b85222d7831acf00/ctranslate2-4.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:d52499f05a60a791aeadee28d609efa130142f376d1ea76b2b1c593bb01f8827", size = 19220784, upload-time = "2026-07-03T12:39:35.74Z" }, + { url = "https://files.pythonhosted.org/packages/c6/66/39111224e418400d97fd79fbc9e72329c51f91a3e7a9c9a1a182e4f88022/ctranslate2-4.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b4c3246aa4a7f309109a841ca743a72cc4abad4f93c0bf7da691023323215621", size = 1271321, upload-time = "2026-07-03T12:39:37.907Z" }, + { url = "https://files.pythonhosted.org/packages/ef/89/13f827fae226eea51315729c00111f716813d7736ebb827fecb8f361fe0d/ctranslate2-4.8.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:c989f747789e8619cbc2e06443b3674c31bc71bad0369652485bd894b627360a", size = 11930735, upload-time = "2026-07-03T12:39:39.534Z" }, + { url = "https://files.pythonhosted.org/packages/c9/94/4b73f9bbaba29df4227cc65114f11d83fe6d696ef3705cb1ade79eb118fd/ctranslate2-4.8.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90eb0bd67b6bb183712cc3fd14bf01ec4f622cd625c5b33cc6c56be7d1c9c34", size = 16872460, upload-time = "2026-07-03T12:39:42.272Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d0/9816494d5ff0745bdf9abe5af04e57a103a416444e604cbe83a6eb0aed7b/ctranslate2-4.8.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e3e3aef4670a6c8dcea367401675f82b49b02c18f5837221bcd7cca90b1707a8", size = 39494736, upload-time = "2026-07-03T12:39:45.733Z" }, + { url = "https://files.pythonhosted.org/packages/6c/dc/22a2c874ca8bb6caa7018dfefdff92dddd487db31cf169891c4c6d408091/ctranslate2-4.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:a2dcce0a57beee984a691d9daa8fc3fd389f5b6cada2644c34571011833bd5b1", size = 19477164, upload-time = "2026-07-03T12:39:48.952Z" }, + { url = "https://files.pythonhosted.org/packages/77/39/7b8d47bf49748ba73182742683eef74b46608beb879765d9d4efc46bc345/ctranslate2-4.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a28c5889585cd17ee3649dfd46d9002ddf50204173f8bff476b9f76d6585795", size = 1293935, upload-time = "2026-07-03T12:39:50.924Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/434e30c752c433eaef5deccd4de54775bc1f205a6fe6c9e756b737018209/ctranslate2-4.8.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:911a5cdef8a405c1804330613a1865f616eb9c092a0e932ee4648128eb20b627", size = 11951789, upload-time = "2026-07-03T12:39:52.886Z" }, + { url = "https://files.pythonhosted.org/packages/85/f2/d716426220b462bbb5bb354b9c6c8d9a41285f067203c860cc79f9f19917/ctranslate2-4.8.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84723cae6f802551bbf2438e5e4810722631a2183b89a82c31df26566b54821d", size = 16860414, upload-time = "2026-07-03T12:39:55.54Z" }, + { url = "https://files.pythonhosted.org/packages/69/11/cdab0e7e2ad4e547f15ab227c09207569f1272abae05816900ecebb0797a/ctranslate2-4.8.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1910752ec541980644191fa3b407bc61dee00e88070b0aed29b4cef75010b3ea", size = 39465200, upload-time = "2026-07-03T12:39:59.017Z" }, + { url = "https://files.pythonhosted.org/packages/c0/03/126e963fc3237a416f3085b8a663ebd8ab449ed6c37195b4e0b49597ba0c/ctranslate2-4.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dc9f1abef55579cc02cdc74b3a55df38491ec56d177d6e6039609d61d09ed30e", size = 19499597, upload-time = "2026-07-03T12:40:01.68Z" }, +] + +[[package]] +name = "dbos" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psycopg", extra = ["binary"] }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "typer-slim" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/b1/8a6c8ace5764e19850f2fd3f010251c3135daf1d682f43566c18dd0d432f/dbos-2.28.0.tar.gz", hash = "sha256:e738d62baf3953639d3e70988912b533ffd9de047d8b90d12755c10c03cac6fe", size = 592187, upload-time = "2026-07-21T15:24:08.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/d4/17f83581209153dff0bb7f6369c83d98d06303f8674746a7d47875add155/dbos-2.28.0-py3-none-any.whl", hash = "sha256:ba1bec5fcb2505b1bdd99251056cecf79669337699a81d5ef2b7598f701f31b6", size = 236036, upload-time = "2026-07-21T15:24:06.354Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "fastapi" +version = "0.140.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/cb/7a4d2c2eb5a5d8a91763c05b7383d72917862e32f780daa0e27ffbb34cc6/fastapi-0.140.13.tar.gz", hash = "sha256:500172a08cf1459901f90b05c37d93060dada3b573fec8f0862445db52ba6b4b", size = 424843, upload-time = "2026-07-28T15:37:00.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4e/f9e8c762ef5e05c40482131e3d5e8b36bca13fa127578261f1d6b35a25d4/fastapi-0.140.13-py3-none-any.whl", hash = "sha256:8b017110e1e9f30a95e8bdb8f71fbe2f0fe3af5717109e5b14f9e069df54f6d4", size = 131222, upload-time = "2026-07-28T15:37:02.124Z" }, +] + +[[package]] +name = "faster-whisper" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "av" }, + { name = "ctranslate2" }, + { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.72" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx2" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/c8/2549fa8ceaaf0bd61114cf392250a107be657233723bc4e65cb91dba934f/genai_prices-0.0.72.tar.gz", hash = "sha256:a7e481d0ea85922fcf48df6864f2491fc81a4f03dcf0ecbd9aa8c2c6d9fcdbe8", size = 82753, upload-time = "2026-07-22T21:11:03.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/2e/1e2c31666f7e4e2d0327a80f1c920fd8764ccd27672c4bc389d79219ece6/genai_prices-0.0.72-py3-none-any.whl", hash = "sha256:21281d8df34d9bfbb736e0763a60ceda02226938cd47379ae437a44fcf8ba23d", size = 85238, upload-time = "2026-07-22T21:11:02.911Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.57" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/0d/132ed135c871b6bf91adf16a0e43797cd535b81d4973b5d09291c54fc5ee/gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488", size = 225898, upload-time = "2026-07-26T07:33:26.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/6e/2139de986d9c7c3ac86f1f8be43858ce90bdfe2f7175e6c80c650ba15242/gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf", size = 217151, upload-time = "2026-07-26T07:33:24.838Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/16/71eefcf68267bbf06a9b6bff57d0b222e49432326e85d74348b67694b8d4/greenlet-3.5.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb", size = 294266, upload-time = "2026-07-22T11:37:56.142Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/a0b19adfc35d07e10acb626e9d22a3893b95f1309c42c4a20161dec16800/greenlet-3.5.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686", size = 613712, upload-time = "2026-07-22T12:26:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a121978b3337407d05a1ce5f79b4aa5998a43a9d8422f9726029b90b4471/greenlet-3.5.4-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7", size = 625582, upload-time = "2026-07-22T12:29:00.814Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4a/f301f1d85c69a86b90b5d581a73e8927bba4e79450037e6e2cbca05eb4fd/greenlet-3.5.4-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9667862a2e38ad379f11b845daeda22c8989186def44f06962c9c4c05e556da7", size = 633429, upload-time = "2026-07-22T12:43:42.073Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/080f16cf870e929e592f55767f01d6c98d2ee83bfdc36c3b892f2d0459ab/greenlet-3.5.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071", size = 624663, upload-time = "2026-07-22T11:51:08.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/26884072b0eb343a4d5fee903341bfe5171b32b7f14553886e2b6349135a/greenlet-3.5.4-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:ae53534b5dec0f4c2ec26f898f538dc8ea1ca3ef2927d597a9439e40a09da937", size = 428238, upload-time = "2026-07-22T12:39:49.973Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/8f3ca88370b817369008faeceeee85970adc16c92a70a3e5fe5fea495a57/greenlet-3.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72", size = 1585010, upload-time = "2026-07-22T12:25:02.539Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/45877154689709ebce9a0b83c2235e6ca0f31577889b02af308c8cc5f8fb/greenlet-3.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59", size = 1651283, upload-time = "2026-07-22T11:51:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/8711a75cb61d85246277c07ff6e1a6504621ba473d808c11ad225ffca43f/greenlet-3.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6", size = 246434, upload-time = "2026-07-22T11:43:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/00/62/e290b3bce433da8f0324ac02da0b128d683482229f1a8b789fa47818a4cd/greenlet-3.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:c38c902a0986eba1f6e7ba1ab39ad5195926abde90f3fe080e08212db62176da", size = 244990, upload-time = "2026-07-22T11:39:22.626Z" }, + { url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f9/03e26be3487c5238e81f2b84714959a86ea8515a869828cf41f4fc54b34e/greenlet-3.5.4-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf", size = 629603, upload-time = "2026-07-22T12:43:43.456Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, + { url = "https://files.pythonhosted.org/packages/57/6b/7c55ca72ef80d57c16c4a55210f82582622462dc4485799a30f4ec6f3372/greenlet-3.5.4-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f", size = 432554, upload-time = "2026-07-22T12:39:51.379Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e3/ef56864b4c35fcb3eb3b41b869f6cc46f4cd3f5e2c68e74acde8ac433951/greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0", size = 245565, upload-time = "2026-07-22T11:38:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/1b/80/fb4d4788bbc8e54761f1fc88533af9523a6e86299fa113d6e8a8503ed9fc/greenlet-3.5.4-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c", size = 632845, upload-time = "2026-07-22T12:43:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/6086fa578ebb72772722cdc4bcd628459814b42e0c2db1e3cbd6552b3271/greenlet-3.5.4-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861", size = 435053, upload-time = "2026-07-22T12:39:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/9c/bf/250c2921c7b585dde12f5239e313ca2dcbc464d161ecca36e4e6ef21762d/greenlet-3.5.4-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c", size = 677968, upload-time = "2026-07-22T12:43:46.788Z" }, + { url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/18/40/10bfcf6513558d82f7b95dd728001c63bd388259fe27d3e30ae01f103430/greenlet-3.5.4-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c", size = 480643, upload-time = "2026-07-22T12:39:54.149Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, + { url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, + { url = "https://files.pythonhosted.org/packages/05/75/625bcdd74d5e6b2dca1ecba3c3ac77bcf8a026c21a649a46cef23e421f97/greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f", size = 246892, upload-time = "2026-07-22T11:40:27.357Z" }, + { url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, + { url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, + { url = "https://files.pythonhosted.org/packages/ae/db/24a10af12bf8e639cec46c38b9ce1a282543ba42ff4fb0b31a970f1ab603/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8", size = 681690, upload-time = "2026-07-22T12:43:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, + { url = "https://files.pythonhosted.org/packages/f4/60/44a2eca7b9fd71ae0fae7ff184da1cd3169d176652b97aa1cffcbb0ef961/greenlet-3.5.4-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd", size = 510263, upload-time = "2026-07-22T12:39:55.678Z" }, + { url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, + { url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, +] + +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + +[[package]] +name = "grpcio" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/f6/3b781cd07a715ea5f5125ae264226e7fc4d87603d6d3955022cabfdc5da2/grpcio-1.83.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f", size = 6338720, upload-time = "2026-07-23T15:19:13.177Z" }, + { url = "https://files.pythonhosted.org/packages/21/cc/d14833d15d5984e366f1b027fa78bd038c9b028c66880bffb0f5a4d25ee2/grpcio-1.83.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223", size = 12178773, upload-time = "2026-07-23T15:19:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/6b/98/8acbb416544e7871132d8e42a07ed70c802d70e6a16c6009e505a34d32a4/grpcio-1.83.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0", size = 6921203, upload-time = "2026-07-23T15:19:17.824Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/0fdbfaf4fc54e5c88f6bce4008a065092fe7fbc4460eb5617ae8b20fd505/grpcio-1.83.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d", size = 7648508, upload-time = "2026-07-23T15:19:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ea/107b9dbb2ed3ad14dd774fd3dde7d29ff9938a6c198654becb2c3a0e9a6a/grpcio-1.83.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9", size = 7079466, upload-time = "2026-07-23T15:19:21.478Z" }, + { url = "https://files.pythonhosted.org/packages/3b/06/9fa9941089e6fae83b060b6ce61c1e81053e52decae43197245f45e07d36/grpcio-1.83.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745", size = 7605583, upload-time = "2026-07-23T15:19:23.74Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/f10fb56062dc2771c630827a82d9ad0ecd05cad572ea3b08d49f6631680a/grpcio-1.83.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617", size = 8637810, upload-time = "2026-07-23T15:19:25.536Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/f84927258f6a1b6ea6dea661fdc6de859b35e560c96f3012d15ccd39f85e/grpcio-1.83.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969", size = 8008021, upload-time = "2026-07-23T15:19:27.863Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/cdf72161397ccd29d4ca2192f641524536c9cf54ad948c9dd0e0e01138fa/grpcio-1.83.0-cp311-cp311-win32.whl", hash = "sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45", size = 4404376, upload-time = "2026-07-23T15:19:30.137Z" }, + { url = "https://files.pythonhosted.org/packages/df/ed/e0ffeb4c848699c194dc9fb6a29ab29bcb2b6aac8c416bf18c51bfe8242c/grpcio-1.83.0-cp311-cp311-win_amd64.whl", hash = "sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49", size = 5164469, upload-time = "2026-07-23T15:19:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0", size = 6304480, upload-time = "2026-07-23T15:19:53.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1", size = 12154419, upload-time = "2026-07-23T15:19:56.3Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867", size = 6873200, upload-time = "2026-07-23T15:19:58.701Z" }, + { url = "https://files.pythonhosted.org/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881", size = 7618811, upload-time = "2026-07-23T15:20:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f", size = 7042310, upload-time = "2026-07-23T15:20:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61", size = 7575412, upload-time = "2026-07-23T15:20:04.712Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45", size = 8604248, upload-time = "2026-07-23T15:20:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c", size = 7977458, upload-time = "2026-07-23T15:20:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf", size = 4393993, upload-time = "2026-07-23T15:20:11.017Z" }, + { url = "https://files.pythonhosted.org/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735", size = 5159650, upload-time = "2026-07-23T15:20:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/9c/60/f2cca8147ea213d3e43ae9158d03ad04e020fdf32ff027253e1fe93f921d/grpcio-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa", size = 6305607, upload-time = "2026-07-23T15:20:15.353Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ab/d3874931d123a95e83a3ebf8aa04537988fb62425cedb8bf3cefc5ad41b2/grpcio-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c", size = 12166617, upload-time = "2026-07-23T15:20:17.435Z" }, + { url = "https://files.pythonhosted.org/packages/92/ff/6f18f9426b69306f4e00a9add3b0ee2748da8aad53836ef80cab0d62d04f/grpcio-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b", size = 6880213, upload-time = "2026-07-23T15:20:19.98Z" }, + { url = "https://files.pythonhosted.org/packages/70/21/706d1147c6b93b98f179240c13991fbcc56880eba0c868abb1ad40d8a0a6/grpcio-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c", size = 7618335, upload-time = "2026-07-23T15:20:22.161Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/1a8443c889115ec9e213a213e86bc93a71ee9088027e5befa09aaa0edd9d/grpcio-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df", size = 7043416, upload-time = "2026-07-23T15:20:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/c6/94e0fee5b12bc1da1370185b680988db6f739d19b42d9959db01a7ea50bf/grpcio-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b", size = 7583253, upload-time = "2026-07-23T15:20:26.313Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/de1ccb671fb85575bc5192faedf9ecdbdf5b390d2e6584dcf552bcbd370e/grpcio-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404", size = 8605102, upload-time = "2026-07-23T15:20:28.437Z" }, + { url = "https://files.pythonhosted.org/packages/17/0f/0e0ec749a7034ffcbaa050e39779872950ead90c22e7e0116be3f28b2b46/grpcio-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af", size = 7979826, upload-time = "2026-07-23T15:20:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/83/fa/c3fda157287f64bc65acee6c5aa90c41acf9e0d3a8e69a265eecff6d00a1/grpcio-1.83.0-cp314-cp314-win32.whl", hash = "sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33", size = 4471765, upload-time = "2026-07-23T15:20:33.195Z" }, + { url = "https://files.pythonhosted.org/packages/a1/00/b1b26431c9d54eee11724fd6e5585473a2ed47fbc1fb95e5204906a642ce/grpcio-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9", size = 5298932, upload-time = "2026-07-23T15:20:35.48Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674, upload-time = "2026-07-16T17:29:56.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/be/525eabac5d1736b679c39e342ecd4292534012546a2d18f0043c8e3b6021/hf_xet-1.5.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b", size = 4064284, upload-time = "2026-07-16T17:29:29.907Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3f/699749dd78442480eda4e4fca494284b0e3542e4063cc37654d5fdc929e6/hf_xet-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576", size = 3828537, upload-time = "2026-07-16T17:29:31.549Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/2658ac0a5b9f4664ca27ce31bd015044fe9dea50ed455fb5197aba819c11/hf_xet-1.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4", size = 4417133, upload-time = "2026-07-16T17:29:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/d9/58/8343f3cb63c8fa058d576136df3871550f7d5214a8f048a7ea2eab6ac906/hf_xet-1.5.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4", size = 4212613, upload-time = "2026-07-16T17:29:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/0c/33/a968f4e4535037b36941ec00714625fb60e026302407e7e26ca9f3e65f4e/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380", size = 4412710, upload-time = "2026-07-16T17:29:36.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/9e33981173dbaf194ba0015202b02d467b624d44d4eba89e1bf06c0d2995/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577", size = 4628455, upload-time = "2026-07-16T17:29:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4b/cc682832de4264a03880a2d1b5ec3e1fab3bf307f508817250baafdb9996/hf_xet-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e", size = 3979044, upload-time = "2026-07-16T17:29:40.329Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/b2cdf2a0fb39a08af3222b96092a36bd3b40c54123eef07de4422e870971/hf_xet-1.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e", size = 3808037, upload-time = "2026-07-16T17:29:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760, upload-time = "2026-07-16T17:29:43.884Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438, upload-time = "2026-07-16T17:29:45.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006, upload-time = "2026-07-16T17:29:46.996Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099, upload-time = "2026-07-16T17:29:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766, upload-time = "2026-07-16T17:29:50.092Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716, upload-time = "2026-07-16T17:29:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373, upload-time = "2026-07-16T17:29:53.395Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957, upload-time = "2026-07-16T17:29:54.991Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpcore2" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/a8/20ed1ed79cbc2ecdf5301c0968ab7c85547212e2a7bd126ddd2d986e206e/httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2", size = 67089, upload-time = "2026-07-24T09:21:03.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fb/46c52b781975c335a2bcf1072c7bbc007cbdc8d674217f5ee1daba2c848b/httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26", size = 82809, upload-time = "2026-07-24T09:21:01.178Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx2" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/14/38128fbafd7e0ed41d874df6c9a653d47c2d111cfe59e2b4ac95161b4abd/httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a", size = 95458, upload-time = "2026-07-24T09:21:04.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/b8/cfd91c4ab9134d386d48f0b6ac662ff3d4be6efdee59ee1c67ebc3c0487c/httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a", size = 91191, upload-time = "2026-07-24T09:21:02.6Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/50/db3771a6e4fad4bd28fb055d4363b51cb0ae98c1aa504b79d41fdcab5483/huggingface_hub-1.25.1.tar.gz", hash = "sha256:21129595ca7a753be479b319913e22cc8808361ac118bd76cc413db831b28a99", size = 928426, upload-time = "2026-07-27T09:24:10.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/21e816831c6d16f88a6c784974413fa0421ce8a5d04380c2666ed5b503e5/huggingface_hub-1.25.1-py3-none-any.whl", hash = "sha256:004d4e70350517e24c68a7dbb7dc5e40b2b6aefef8f94bf7a85f6f9835102ea5", size = 774909, upload-time = "2026-07-27T09:24:08.079Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +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" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +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" }, +] +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 = "kubernetes" +version = "36.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/57/b07b96353f902aa1bdbe00e878e3a12a137977d03a962479785576aa8ec9/kubernetes-36.0.3.tar.gz", hash = "sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7", size = 2337528, upload-time = "2026-07-13T20:38:12.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/30/a96d47df739689ac0001ade0afefc16e3b477fc2fb426b568515fdc8afce/kubernetes-36.0.3-py2.py3-none-any.whl", hash = "sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f", size = 4618066, upload-time = "2026-07-13T20:38:10.172Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.39.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/f4/41f8647f6091fb9b9aac5a4b6d164bddb11d55b8369bf38de154ef91b8f4/logfire_api-4.39.0.tar.gz", hash = "sha256:1e885f95c37d58cdb927bbc6baea4f4a7c13066f6b3019758627d4dc442643d0", size = 90619, upload-time = "2026-07-24T18:31:36.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/d4/87747d12eaf2d852676fd6535df76df945ef62681f1eb5391b63d1fc05e2/logfire_api-4.39.0-py3-none-any.whl", hash = "sha256:20057bbd2898dec2eed02e2559bd73f4e10bc4b108987821df55e9c762da3ba8", size = 140413, upload-time = "2026-07-24T18:31:32.818Z" }, +] + +[[package]] +name = "mako" +version = "1.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mcp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx2" }, + { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, + { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, + { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, + { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794, upload-time = "2026-03-05T15:55:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923, upload-time = "2026-03-05T15:55:10.924Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131, upload-time = "2026-03-05T15:55:11.961Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825, upload-time = "2026-03-05T15:55:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344, upload-time = "2026-03-05T15:55:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291, upload-time = "2026-03-05T15:55:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575, upload-time = "2026-03-05T15:55:16.518Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052, upload-time = "2026-03-05T15:55:17.735Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311, upload-time = "2026-03-05T15:55:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279, upload-time = "2026-03-05T15:55:20.448Z" }, + { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290, upload-time = "2026-03-05T15:55:21.6Z" }, + { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116, upload-time = "2026-03-05T15:55:22.826Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740, upload-time = "2026-03-05T15:55:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143, upload-time = "2026-03-05T15:55:25.456Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703, upload-time = "2026-03-05T15:55:26.723Z" }, + { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484, upload-time = "2026-03-05T15:55:28.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012, upload-time = "2026-03-05T15:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508, upload-time = "2026-03-05T15:55:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387, upload-time = "2026-03-05T15:55:32.403Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101, upload-time = "2026-03-05T15:55:33.646Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836, upload-time = "2026-03-05T15:55:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727, upload-time = "2026-03-05T15:55:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308, upload-time = "2026-03-05T15:55:37.254Z" }, + { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758, upload-time = "2026-03-05T15:55:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670, upload-time = "2026-03-05T15:55:40.13Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399, upload-time = "2026-03-05T15:55:42.076Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386, upload-time = "2026-03-05T15:55:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924, upload-time = "2026-03-05T15:55:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280, upload-time = "2026-03-05T15:55:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050, upload-time = "2026-03-05T15:55:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158, upload-time = "2026-03-05T15:55:48.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890, upload-time = "2026-03-05T15:55:49.405Z" }, + { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121, upload-time = "2026-03-05T15:55:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187, upload-time = "2026-03-05T15:55:52.143Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934, upload-time = "2026-03-05T15:55:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036, upload-time = "2026-03-05T15:55:55.252Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "narwhals" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')", + "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", + "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", + "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')", + "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4d/5014667e2a3a77d6e1b74cc3d88948d06163b8e0a33a84c85073322b5dec/onnxruntime-1.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f5c5daabd28aad610f83fdcf32acec8fb57e6adc6c6a39fe2a3c755db957b410", size = 19130506, upload-time = "2026-07-25T01:22:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/97/b7ce1bc8bb6048b5fe9129f55d6506dc19499068ef2e0a0af1ae3c8aa4e7/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d66f9ceb29909c70839e4e4fb3435c7b490050d8f162bd5f3aba4ca01ee517f", size = 17039880, upload-time = "2026-07-25T01:21:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/f3/17/4e5ecd8764f87573c495d834ce79e61ecca47f7a01d1e444a606e570edcb/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a166b78ee04f3a37fa1ef82034b6a3ce96d9684e582d4d30b296de83e9998bb5", size = 19193162, upload-time = "2026-07-25T01:21:59.151Z" }, + { url = "https://files.pythonhosted.org/packages/9f/10/3d946d5d5f2cdcc3c8da36cae63190c516d16349edaffd944bda60ca4c3e/onnxruntime-1.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:0d650aeee29368414367b65529e90afe4bf1bab76254789063b8b2f7ea3013c8", size = 13752539, upload-time = "2026-07-25T01:22:24.524Z" }, + { url = "https://files.pythonhosted.org/packages/8f/74/1c440be7af1e026280b139caa1be5d11bd4dc368011ddbe8f5362b58e12f/onnxruntime-1.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:0faf85fb447a663c9cdadc39bd6b19bdf7bedded6699e45731b9b36c46fd993d", size = 13449940, upload-time = "2026-07-25T01:22:14.97Z" }, + { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362, upload-time = "2026-07-25T01:22:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628, upload-time = "2026-07-25T01:21:40.481Z" }, + { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257, upload-time = "2026-07-25T01:22:01.695Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036, upload-time = "2026-07-25T01:22:26.89Z" }, + { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462, upload-time = "2026-07-25T01:22:17.38Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759, upload-time = "2026-07-25T01:21:53.765Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339, upload-time = "2026-07-25T01:21:43.005Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329, upload-time = "2026-07-25T01:22:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033, upload-time = "2026-07-25T01:22:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175, upload-time = "2026-07-25T01:22:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307, upload-time = "2026-07-25T01:21:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954, upload-time = "2026-07-25T01:22:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748, upload-time = "2026-07-25T01:21:56.297Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950, upload-time = "2026-07-25T01:21:48.606Z" }, + { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924, upload-time = "2026-07-25T01:22:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738, upload-time = "2026-07-25T01:22:31.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117, upload-time = "2026-07-25T01:22:22.387Z" }, + { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518, upload-time = "2026-07-25T01:21:51.08Z" }, + { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976, upload-time = "2026-07-25T01:22:12.474Z" }, +] + +[[package]] +name = "openai" +version = "2.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/f5/e7735f2af272ee179a287911a698b3cbdb59d7a4ac4874571363adf1e4de/openai-2.50.0.tar.gz", hash = "sha256:5128f7caf4a6b01aefd6e7e93efe170a2c3427b8de286b9af5cdff3aa47e02c8", size = 1081965, upload-time = "2026-07-28T22:16:31.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/ca/db315b3bb748c26c644a3f85b7d509e774354d6518d47080b1446005ee41/openai-2.50.0-py3-none-any.whl", hash = "sha256:90bdddcc5a2fa529b350fac9c5780d87e5c361dcc6090ab57b0d470b0d7af7fa", size = 1650721, upload-time = "2026-07-28T22:16:29.48Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "5.0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/99/76b7c80252aa83c1af16393454aafd125a0287101afe8deb0a6821af0e30/opencv_python_headless-5.0.0.93.tar.gz", hash = "sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c", size = 81817738, upload-time = "2026-07-02T07:01:06.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7c/8c8097891c509d98cd128493835c95631c80be6a8f37ed9d25716c2e16f1/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f", size = 48322581, upload-time = "2026-07-02T05:50:34.207Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/eab2ad388c3cbab2a350c10c2ef19ce6bd099240afc31789032c996bab52/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00", size = 34782894, upload-time = "2026-07-02T05:51:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/ec/78/afca939f40ffe2b2380bfa86f812b2f7d4acc5a27b27dc41b49cad7ce7b4/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f", size = 36521085, upload-time = "2026-07-02T06:55:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/2b/97/8170e9819764c47e436c130d3ff6cfb73b58f923eae9d3a03d8982b04aec/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4", size = 56563598, upload-time = "2026-07-02T06:55:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/1a28a7101e31801042b3098871a74b76c61581d328ef40774ff4edb53a56/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4", size = 39648433, upload-time = "2026-07-02T06:56:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/9b/21/f6ef335f6e65724aa78b8d792b48d40a48c381715f1e62f5a5049e09d07e/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37", size = 61204038, upload-time = "2026-07-02T06:56:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8f/b8756467ea991449a293797f6b3fa80fcfdd29598a0a60d1cd5715b96e61/opencv_python_headless-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9", size = 35411237, upload-time = "2026-07-02T05:50:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/b8/88/763b967f7efd7226b82c9fae16d560cba049b1f0c036647e65c610fd636e/opencv_python_headless-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e", size = 43825962, upload-time = "2026-07-02T05:50:09.627Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/82/df3312c0ca083d5b43b352f27d4dd8b1e614bd334473074715d9e0000da4/psycopg_binary-3.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da", size = 4609813, upload-time = "2026-05-01T23:26:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/d74d542458d3e8ac0571d8a88f57ca369999b9a82f4fa528052d0d7d3e4c/psycopg_binary-3.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e", size = 4676799, upload-time = "2026-05-01T23:26:38.475Z" }, + { url = "https://files.pythonhosted.org/packages/09/67/06bab9c60671999f4c6ceff1b334f3ac1f9fc5789eb467c714623ea21de9/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992", size = 5497050, upload-time = "2026-05-01T23:26:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/72/9b/023433e2b20f970de1e22d29132a95281277646da0b2e2879dd4ee94b8c1/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4", size = 5172428, upload-time = "2026-05-01T23:26:56.708Z" }, + { url = "https://files.pythonhosted.org/packages/08/cd/ae16da8fde228a38b2fe9269bbc13cf89e0186173f2265600f02d6a71e64/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e", size = 6762746, upload-time = "2026-05-01T23:27:07.023Z" }, + { url = "https://files.pythonhosted.org/packages/4f/81/0ba09fa5f5f88779093a2541a8e02489825721f258ab88058b11d68b3eb5/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097", size = 5006033, upload-time = "2026-05-01T23:27:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/73/6a/629136040cc3497adb442a305710b5913f2a754d4630fc3d3717c4c0df65/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95", size = 4534175, upload-time = "2026-05-01T23:27:18.248Z" }, + { url = "https://files.pythonhosted.org/packages/7c/32/1027f843c6dc2d5d51960ee62cc0c2cf755a4c39455aff1371173edbef7d/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839", size = 4224203, upload-time = "2026-05-01T23:27:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/380a724d9093c74adb14d4fce920ea8327838abb61f760b1448586b14a8e/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007", size = 3954509, upload-time = "2026-05-01T23:27:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/db/cd/895893ae575a09c97ccfd5def070d88993d955ef34df45a881fd5ff506d6/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c", size = 4259551, upload-time = "2026-05-01T23:27:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/2330a20794e37a3ec609ef2fd8522919ec7a4395a1abf979a8e2d1775cd5/psycopg_binary-3.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d", size = 3572054, upload-time = "2026-05-01T23:27:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/63/21e981e9d3f1f123e0b0ee2130112b1956cad9752309f574862c7ae77c08/pybase64-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70b0d4a4d54e216ce42c2655315378b8903933ecfa32fced453989a92b4317b2", size = 38237, upload-time = "2025-12-06T13:22:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/92/fb/3f448e139516404d2a3963915cc10dc9dde7d3a67de4edba2f827adfef17/pybase64-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8127f110cdee7a70e576c5c9c1d4e17e92e76c191869085efbc50419f4ae3c72", size = 31673, upload-time = "2025-12-06T13:22:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, + { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, + { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/8338b6d3da3c265002839e92af0a80d6db88385c313c73f103dfb800c857/pybase64-1.4.3-cp311-cp311-win32.whl", hash = "sha256:e9a8b81984e3c6fb1db9e1614341b0a2d98c0033d693d90c726677db1ffa3a4c", size = 33639, upload-time = "2025-12-06T13:23:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/39/dc/32efdf2f5927e5449cc341c266a1bbc5fecd5319a8807d9c5405f76e6d02/pybase64-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:a90a8fa16a901fabf20de824d7acce07586e6127dc2333f1de05f73b1f848319", size = 35797, upload-time = "2025-12-06T13:23:13.174Z" }, + { url = "https://files.pythonhosted.org/packages/da/59/eda4f9cb0cbce5a45f0cd06131e710674f8123a4d570772c5b9694f88559/pybase64-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:61d87de5bc94d143622e94390ec3e11b9c1d4644fe9be3a81068ab0f91056f59", size = 31160, upload-time = "2025-12-06T13:23:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" }, + { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" }, + { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" }, + { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" }, + { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" }, + { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, + { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, + { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/545fd4935a0e1ddd7147f557bf8157c73eecec9cffd523382fa7af2557de/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_10_9_x86_64.whl", hash = "sha256:d27c1dfdb0c59a5e758e7a98bd78eaca5983c22f4a811a36f4f980d245df4611", size = 38393, upload-time = "2025-12-06T13:26:19.535Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/ae7a96be9ddc96030d4e9dffc43635d4e136b12058b387fd47eb8301b60f/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0f1a0c51d6f159511e3431b73c25db31095ee36c394e26a4349e067c62f434e5", size = 32109, upload-time = "2025-12-06T13:26:20.72Z" }, + { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5b/19c725dc3aaa6281f2ce3ea4c1628d154a40dd99657d1381995f8096768b/pybase64-1.4.3-graalpy311-graalpy242_311_native-win_amd64.whl", hash = "sha256:03cea70676ffbd39a1ab7930a2d24c625b416cacc9d401599b1d29415a43ab6a", size = 35880, upload-time = "2025-12-06T13:26:24.663Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/160dded493c00d3376d4ad0f38a2119c5345de4a6693419ad39c3565959b/pybase64-1.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:277de6e03cc9090fb359365c686a2a3036d23aee6cd20d45d22b8c89d1247f17", size = 37939, upload-time = "2025-12-06T13:26:41.014Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/a0f10be8d648d6f8f26e560d6e6955efa7df0ff1e009155717454d76f601/pybase64-1.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab1dd8b1ed2d1d750260ed58ab40defaa5ba83f76a30e18b9ebd5646f6247ae5", size = 31466, upload-time = "2025-12-06T13:26:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/53/7bfa293ed6d2eb3c400c14d442867bc22cba3cf5f1551b829f54f18ff693/pydantic_ai_slim-2.20.0.tar.gz", hash = "sha256:d70eea3c82a5976d992542ebe2bdbd1af63cd41d9d896b0d59f061fadf42cd2b", size = 912253, upload-time = "2026-07-29T03:37:50.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/3e/d5377980b29e6b18ee334f31cdbdb55dbfe437358251406489e683bde631/pydantic_ai_slim-2.20.0-py3-none-any.whl", hash = "sha256:c61f1e9a19cb85028638e4909f64b8331ed754e918d8bf892434fd92b1f93f59", size = 1101608, upload-time = "2026-07-29T03:37:41.463Z" }, +] + +[package.optional-dependencies] +openai = [ + { name = "openai" }, + { name = "tiktoken" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/b2/63e33bec7f5d95a40af8503c83d66cda151315a16d334975209875766d4d/pydantic_graph-2.20.0.tar.gz", hash = "sha256:d009dc97689ede3d20e2d99384f93b867ccd5b408dcc394551b8dd26cfcde80f", size = 44150, upload-time = "2026-07-29T03:37:52.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/24/1dd8d79a01ec0278ce9dc0933fe3a5c5bb8cde34e717294b9b028c539f6e/pydantic_graph-2.20.0-py3-none-any.whl", hash = "sha256:ceee415418a8cc6f8ef6968c8343e6be00c1971157dc552fc1fbb24790b5250a", size = 51658, upload-time = "2026-07-29T03:37:45.131Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pydeck" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/c9/f71032fca47ecc09d30904d9a610234b07139f89eabc2f054b141edcc30f/pydeck-0.9.3.tar.gz", hash = "sha256:695775cbfe51f5fdffbd9735ba469987fdc5efc96bc40a0ee4808170509c78b2", size = 5900912, upload-time = "2026-07-02T23:27:08.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/34/3998411437aff304a9ed4fa37a6fe1ef3132bcd2b5eac59851b80c86123c/pydeck-0.9.3-py2.py3-none-any.whl", hash = "sha256:d8a47c11c81fb12d51b1feb42427ff4f0e13cb599e48931021b2cba98b6849a6", size = 11428091, upload-time = "2026-07-02T23:27:06.399Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pypika" +version = "0.51.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919, upload-time = "2026-02-04T11:27:48.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585, upload-time = "2026-02-04T11:27:46.251Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +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 = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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.7.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e5/cef4de2bac939280b68d32adc659478845238a8274f2f79c465063f590ad/regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c", size = 494012, upload-time = "2026-07-19T00:16:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/ff/87/e86f51eb117457bb7803132ffe5cb6e2841e2b5bea4cc85d397f3c6e257d/regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae", size = 295281, upload-time = "2026-07-19T00:16:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/2360c41d8080a3d9ec7e5c90fad6eab3b50192869d10e9a5609e48c8177b/regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5", size = 290615, upload-time = "2026-07-19T00:16:43.058Z" }, + { url = "https://files.pythonhosted.org/packages/cf/69/b65ba4344efbc771b28fe5dde84cbbb6c8f9551165952fe78def5b9dde6a/regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20", size = 791804, upload-time = "2026-07-19T00:16:44.662Z" }, + { url = "https://files.pythonhosted.org/packages/81/b6/a40dfa0dc6224b36f620c00296eacc830489cbf8c2837b6750dfe6170375/regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a", size = 861723, upload-time = "2026-07-19T00:16:46.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/02/735991dee71abd83196a7962f7ed8bf5aa05720ff06e2d3ff896a85e2bbb/regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7", size = 905932, upload-time = "2026-07-19T00:16:47.956Z" }, + { url = "https://files.pythonhosted.org/packages/45/6c/e7098d8b846ccdbf431d8c081b61e496526a27a28094ed09e0dce21b3f54/regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc", size = 801407, upload-time = "2026-07-19T00:16:49.43Z" }, + { url = "https://files.pythonhosted.org/packages/8a/18/34b69274e2649bcc7d9b089c2b2983fb2632d8ecf667e359593be9072e79/regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864", size = 774448, upload-time = "2026-07-19T00:16:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e6/0a72247d025585fd3800b98e040b84d562a88af6303347100484849f4f01/regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af", size = 783297, upload-time = "2026-07-19T00:16:53.071Z" }, + { url = "https://files.pythonhosted.org/packages/b1/aa/c4f65ae7dd02a36b323a70c4cff326e1f3442361aaebc9311100a130d54f/regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5", size = 854736, upload-time = "2026-07-19T00:16:54.607Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/668082bcc817b9e694189b84997aeba7385b7779faa6711788679c482e35/regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82", size = 763298, upload-time = "2026-07-19T00:16:56.289Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fb/2d07ad555e7af88aa5f867fdafa47a8d945ee237c20af3ebceb46a820835/regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78", size = 844430, upload-time = "2026-07-19T00:16:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/51/15/c82a471fe3dce56f03745635b43aa456c40dc0db089e07ef148b331507d1/regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327", size = 789683, upload-time = "2026-07-19T00:16:59.583Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f4/7532a2c59d56f5398902c20de60f0c9a5d1cd364e42a051b48e1b210be7b/regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d", size = 266778, upload-time = "2026-07-19T00:17:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/cf1bc631db154eb95520d9d5dbc2371ff77a0f014bbf7d748fed8496aa63/regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965", size = 277983, upload-time = "2026-07-19T00:17:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bd/56ceaf170e875d5a6761bf2bfd0d040f1cacc896850d5e40cb29b11bbd06/regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e", size = 276961, upload-time = "2026-07-19T00:17:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, + { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, + { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, + { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, + { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, + { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, + { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, + { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, + { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" }, + { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" }, + { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" }, + { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" }, + { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" }, + { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" }, + { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" }, + { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "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/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 = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')", + "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", + "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", + "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')", + "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scikit-learn" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/80/573ab31b77bdfa8f18051188adff3405e928386287cd6f756eff5777dd82/sentence_transformers-5.6.1.tar.gz", hash = "sha256:16af5d682ef66672b076d58599a23905800e850ec2bfb1865938306bf684ad72", size = 452185, upload-time = "2026-07-23T14:40:41.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ad/8f73f512dc7ad4031d2b64cbb67f70bdfb355756afbe0db610a5146415c1/sentence_transformers-5.6.1-py3-none-any.whl", hash = "sha256:cefbb17b6325a982a4732c8c49fb013375392687049d1de3d435c4b04060680b", size = 596677, upload-time = "2026-07-23T14:40:40.312Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, + { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + +[[package]] +name = "srt" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" } + +[[package]] +name = "sse-starlette" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627", size = 32542, upload-time = "2026-07-20T14:16:32.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6", size = 16516, upload-time = "2026-07-20T14:16:30.978Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "streamlit" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "anyio" }, + { name = "blinker" }, + { name = "click" }, + { name = "gitpython" }, + { name = "httptools" }, + { name = "itsdangerous" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "python-multipart" }, + { name = "requests" }, + { name = "starlette" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/46/fa437e9651af1dbe7e5b28a754fd9c36df656e4a14a2b5ecc1bcd1d90587/streamlit-1.60.0.tar.gz", hash = "sha256:09f2fdd2aabbd3b3560953795681dd675d5479ac82c21fc2dceadaf9bd484ea3", size = 9819884, upload-time = "2026-07-21T21:13:58.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/69/7a1984df015d01875c9ac79bfda7c31492cc94f81a2625303ce4707188e5/streamlit-1.60.0-py3-none-any.whl", hash = "sha256:d167d67cf94537600a6e378d8aa0c31eabab26beb243e2a51de9842e2872e393", size = 10419153, upload-time = "2026-07-21T21:13:55.113Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, + { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "fsspec", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "jinja2", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "networkx", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "setuptools", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "sympy", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", + "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", + "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')", + "(python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "fsspec", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jinja2", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "networkx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "setuptools", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "sympy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:6e9817dbdf5ea76789babd46e457eac5bf14ff566cf85f8addbfdff2d56601ce", upload-time = "2026-07-08T19:27:52Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:84453b69508ec79902f899c5ed9495acb9e2bbe9fda5f1d5d6f19e3c3842e1a7", upload-time = "2026-07-08T19:28:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6746dbcbeb526eb61330b76b41ff1b4eb848951103a892eeb080dfa2b264667b", upload-time = "2026-07-08T19:28:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:10717d8b3b67c45a4788bf7ffc0bab1ea1e5ebbedd24466be6100102d141fac1", upload-time = "2026-07-08T19:28:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:2b3d093abd919ad934c43d47e73ba63ceba7cbd7269fc2e9c1e4fc29e8fe45fa", upload-time = "2026-07-08T19:28:33Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:ffadde149901c8afa138daa38d898264003cfcf1a3336ca5cd964b5af227d867", upload-time = "2026-07-08T19:28:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f307c2c32d764ffc6ff6893b801fad6d4752f3e67966cb8abf1843427c02604", upload-time = "2026-07-08T19:28:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ca4a9394b0c771238a4f73590fdbbc4debad85ed0fa63d026ae1b085da7d6e2", upload-time = "2026-07-08T19:29:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:a8b450c1e58e5800e5b4691dac412f8d2d65a1dc3298166f91596603a3531e6f", upload-time = "2026-07-08T19:29:15Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:fa0762705b933624d59f6823db9ce7ec2e35b3e1e9c319c9db51fbeecfc3e319", upload-time = "2026-07-08T19:29:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:966d020354f465672dc7dd10d3a5c6cd17d7eb48620aa1d265b48a1f78f06898", upload-time = "2026-07-08T19:29:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0b8f7d0423027ae8b90c7977c627f3379f325363a08224dffad9b4b2d684a83d", upload-time = "2026-07-08T19:29:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3fbf9c9d1f3c10c2d59d04aca426dee9ccc6ceb32d255c61e93acc3b4f75fae6", upload-time = "2026-07-08T19:29:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:a17ff48608634db245e17e8bb00a9558554a49aeb1e4f5fe6cd039af2a10515b", upload-time = "2026-07-08T19:30:05Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:ac7aaf322be4777765a53bed7264a214dd81b3a1d276b93150515a3c5f75e4b0", upload-time = "2026-07-08T19:30:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:dec241fef3984c0d1edadd1f58708e218d4eae881ceef7bc10cf9964d41b68b9", upload-time = "2026-07-08T19:30:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ca021f9eb2f8345c83fa03e3a04587308afb8df71bd472670b3ece00df58621c", upload-time = "2026-07-08T19:30:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d20fa53ee744502fa4c69818a720b05ca0d37abd055d4f6e66cae155114bc691", upload-time = "2026-07-08T19:30:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:e2e5134decf00e218da62318f3dc5df156231d367871918e91eba95ab0ad43ab", upload-time = "2026-07-08T19:30:58Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:991cc14b39e751122c01f017be6448533989868731cb5eecd1006893d26787c2", upload-time = "2026-07-08T19:31:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7b8d26e29bceafbdaa8d63bfe7612f23875b5af2cc07e13f809c3ed890bbe1d8", upload-time = "2026-07-08T19:31:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b222c15a0fc2ce207d1c1a59700b46c8fa6748df1f447ad11e5c870dde0933d9", upload-time = "2026-07-08T19:31:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:a43376bd094124ef626bfdd3d4c2c62eacb0b5ddc99776f4a32d4fd16f1f3420", upload-time = "2026-07-08T19:31:48Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "transformers" +version = "5.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/fb/2a2ba88f325e68a921d8b69ff63b477830b2e73ade9a3c8c8cab2f06d741/transformers-5.14.1.tar.gz", hash = "sha256:60d196c27781eacf8637e2b533f517582907ad6f9ae142046d6b69431a5b2173", size = 9295927, upload-time = "2026-07-16T09:41:57.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/67/8d85ca2323233ae3c0365a659c4e52ee1f587b440e4bc577e7d8e4416d0f/transformers-5.14.1-py3-none-any.whl", hash = "sha256:9db974c4079ede2d1a3ea7ca5a240df33f2cc26fc2b36ba64c5f2a4f43b6e725", size = 11625234, upload-time = "2026-07-16T09:41:54.143Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "vidxp" +version = "0.2.1b1" +source = { editable = "." } +dependencies = [ + { name = "dbos" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "rich" }, + { name = "sqlalchemy" }, + { name = "typer" }, +] + +[package.optional-dependencies] +actor = [ + { name = "chromadb" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "opencv-python-headless" }, + { name = "pooch" }, + { name = "psutil" }, +] +all = [ + { name = "chromadb" }, + { name = "faster-whisper" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "opencv-python-headless" }, + { name = "pillow" }, + { name = "pooch" }, + { name = "psutil" }, + { name = "sentence-transformers" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers" }, +] +benchmarks = [ + { name = "srt" }, +] +dialogue = [ + { name = "chromadb" }, + { name = "faster-whisper" }, + { name = "huggingface-hub" }, + { name = "psutil" }, + { name = "sentence-transformers" }, +] +frontend = [ + { name = "streamlit" }, +] +local-worker = [ + { name = "chromadb" }, + { name = "faster-whisper" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "opencv-python-headless" }, + { name = "pillow" }, + { name = "pooch" }, + { name = "psutil" }, + { name = "pydantic-ai-slim", extra = ["openai"] }, + { name = "sentence-transformers" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers" }, +] +mcp = [ + { name = "mcp" }, +] +scene = [ + { name = "chromadb" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "opencv-python-headless" }, + { name = "pillow" }, + { name = "psutil" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers" }, +] +server = [ + { name = "alembic" }, + { name = "asgi-correlation-id" }, + { name = "fastapi" }, + { name = "mcp" }, + { name = "psutil" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] +server-worker = [ + { name = "alembic" }, + { name = "asgi-correlation-id" }, + { name = "chromadb-client" }, + { name = "fastapi" }, + { name = "faster-whisper" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "opencv-python-headless" }, + { name = "pillow" }, + { name = "pooch" }, + { name = "psutil" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pydantic-ai-slim", extra = ["openai"] }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "sentence-transformers" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers" }, + { name = "uvicorn", extra = ["standard"] }, +] +slm = [ + { name = "pydantic-ai-slim", extra = ["openai"] }, +] +storage = [ + { name = "chromadb" }, + { name = "psutil" }, +] +test = [ + { name = "httpx" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", marker = "extra == 'server'", specifier = ">=1.18.5,<2" }, + { name = "alembic", marker = "extra == 'server-worker'", specifier = ">=1.18.5,<2" }, + { name = "asgi-correlation-id", marker = "extra == 'server'", specifier = ">=5.0.1,<6" }, + { name = "asgi-correlation-id", marker = "extra == 'server-worker'", specifier = ">=5.0.1,<6" }, + { name = "chromadb", marker = "extra == 'actor'", specifier = ">=1.5.9,<2" }, + { name = "chromadb", marker = "extra == 'all'", specifier = ">=1.5.9,<2" }, + { name = "chromadb", marker = "extra == 'dialogue'", specifier = ">=1.5.9,<2" }, + { name = "chromadb", marker = "extra == 'local-worker'", specifier = ">=1.5.9,<2" }, + { name = "chromadb", marker = "extra == 'scene'", specifier = ">=1.5.9,<2" }, + { name = "chromadb", marker = "extra == 'storage'", specifier = ">=1.5.9,<2" }, + { name = "chromadb-client", marker = "extra == 'server-worker'", specifier = ">=1.5.9,<2" }, + { name = "dbos", specifier = ">=2.28,<3" }, + { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.140.13,<0.141" }, + { name = "fastapi", marker = "extra == 'server-worker'", specifier = ">=0.140.13,<0.141" }, + { name = "faster-whisper", marker = "extra == 'all'", specifier = ">=1.2.1,<2" }, + { name = "faster-whisper", marker = "extra == 'dialogue'", specifier = ">=1.2.1,<2" }, + { name = "faster-whisper", marker = "extra == 'local-worker'", specifier = ">=1.2.1,<2" }, + { name = "faster-whisper", marker = "extra == 'server-worker'", specifier = ">=1.2.1,<2" }, + { name = "filelock", specifier = ">=3.32,<4" }, + { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.1,<0.29" }, + { name = "huggingface-hub", marker = "extra == 'all'", specifier = ">=1.25.1,<2" }, + { name = "huggingface-hub", marker = "extra == 'dialogue'", specifier = ">=1.25.1,<2" }, + { name = "huggingface-hub", marker = "extra == 'local-worker'", specifier = ">=1.25.1,<2" }, + { name = "huggingface-hub", marker = "extra == 'scene'", specifier = ">=1.25.1,<2" }, + { name = "huggingface-hub", marker = "extra == 'server-worker'", specifier = ">=1.25.1,<2" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=2.0,<3" }, + { name = "mcp", marker = "extra == 'server'", specifier = ">=2.0,<3" }, + { name = "numpy", marker = "extra == 'actor'", specifier = ">=2.3,<3" }, + { name = "numpy", marker = "extra == 'all'", specifier = ">=2.3,<3" }, + { name = "numpy", marker = "extra == 'local-worker'", specifier = ">=2.3,<3" }, + { name = "numpy", marker = "extra == 'scene'", specifier = ">=2.3,<3" }, + { name = "numpy", marker = "extra == 'server-worker'", specifier = ">=2.3,<3" }, + { name = "opencv-python-headless", marker = "extra == 'actor'", specifier = ">=5.0.0.93,<6" }, + { name = "opencv-python-headless", marker = "extra == 'all'", specifier = ">=5.0.0.93,<6" }, + { name = "opencv-python-headless", marker = "extra == 'local-worker'", specifier = ">=5.0.0.93,<6" }, + { name = "opencv-python-headless", marker = "extra == 'scene'", specifier = ">=5.0.0.93,<6" }, + { name = "opencv-python-headless", marker = "extra == 'server-worker'", specifier = ">=5.0.0.93,<6" }, + { name = "packaging", specifier = ">=26.2,<27" }, + { name = "pillow", marker = "extra == 'all'", specifier = ">=12.3,<13" }, + { name = "pillow", marker = "extra == 'local-worker'", specifier = ">=12.3,<13" }, + { name = "pillow", marker = "extra == 'scene'", specifier = ">=12.3,<13" }, + { name = "pillow", marker = "extra == 'server-worker'", specifier = ">=12.3,<13" }, + { name = "platformdirs", specifier = ">=4.11,<5" }, + { name = "pooch", marker = "extra == 'actor'", specifier = ">=1.9,<2" }, + { name = "pooch", marker = "extra == 'all'", specifier = ">=1.9,<2" }, + { name = "pooch", marker = "extra == 'local-worker'", specifier = ">=1.9,<2" }, + { name = "pooch", marker = "extra == 'server-worker'", specifier = ">=1.9,<2" }, + { name = "psutil", marker = "extra == 'actor'", specifier = ">=7.2.2,<8" }, + { name = "psutil", marker = "extra == 'all'", specifier = ">=7.2.2,<8" }, + { name = "psutil", marker = "extra == 'dialogue'", specifier = ">=7.2.2,<8" }, + { name = "psutil", marker = "extra == 'local-worker'", specifier = ">=7.2.2,<8" }, + { name = "psutil", marker = "extra == 'scene'", specifier = ">=7.2.2,<8" }, + { name = "psutil", marker = "extra == 'server'", specifier = ">=7.2.2,<8" }, + { name = "psutil", marker = "extra == 'server-worker'", specifier = ">=7.2.2,<8" }, + { name = "psutil", marker = "extra == 'storage'", specifier = ">=7.2.2,<8" }, + { name = "psycopg", extras = ["binary"], marker = "extra == 'server'", specifier = ">=3.3.4,<4" }, + { name = "psycopg", extras = ["binary"], marker = "extra == 'server-worker'", specifier = ">=3.3.4,<4" }, + { name = "pydantic", specifier = ">=2.13.4,<3" }, + { name = "pydantic-ai-slim", extras = ["openai"], marker = "extra == 'local-worker'", specifier = ">=2.13,<3" }, + { name = "pydantic-ai-slim", extras = ["openai"], marker = "extra == 'server-worker'", specifier = ">=2.13,<3" }, + { name = "pydantic-ai-slim", extras = ["openai"], marker = "extra == 'slm'", specifier = ">=2.13,<3" }, + { name = "pydantic-settings", specifier = ">=2.14.2,<3" }, + { name = "pyjwt", extras = ["crypto"], marker = "extra == 'server'", specifier = ">=2.13,<3" }, + { name = "pyjwt", extras = ["crypto"], marker = "extra == 'server-worker'", specifier = ">=2.13,<3" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=9.1.1,<10" }, + { name = "python-multipart", marker = "extra == 'server'", specifier = ">=0.0.32,<0.1" }, + { name = "python-multipart", marker = "extra == 'server-worker'", specifier = ">=0.0.32,<0.1" }, + { name = "rich", specifier = ">=15,<16" }, + { name = "sentence-transformers", marker = "extra == 'all'", specifier = ">=5.6.1,<6" }, + { name = "sentence-transformers", marker = "extra == 'dialogue'", specifier = ">=5.6.1,<6" }, + { name = "sentence-transformers", marker = "extra == 'local-worker'", specifier = ">=5.6.1,<6" }, + { name = "sentence-transformers", marker = "extra == 'server-worker'", specifier = ">=5.6.1,<6" }, + { name = "sqlalchemy", specifier = ">=2.0.51,<2.1" }, + { name = "srt", marker = "extra == 'benchmarks'", specifier = ">=3.5,<4" }, + { name = "streamlit", marker = "extra == 'frontend'", specifier = ">=1.60,<2" }, + { name = "torch", marker = "(sys_platform == 'linux' and extra == 'all') or (sys_platform == 'win32' and extra == 'all')", specifier = ">=2.13,<3", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", marker = "(sys_platform == 'linux' and extra == 'local-worker') or (sys_platform == 'win32' and extra == 'local-worker')", specifier = ">=2.13,<3", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", marker = "(sys_platform == 'linux' and extra == 'scene') or (sys_platform == 'win32' and extra == 'scene')", specifier = ">=2.13,<3", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", marker = "(sys_platform == 'linux' and extra == 'server-worker') or (sys_platform == 'win32' and extra == 'server-worker')", specifier = ">=2.13,<3", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32' and extra == 'all'", specifier = ">=2.13,<3" }, + { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32' and extra == 'local-worker'", specifier = ">=2.13,<3" }, + { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32' and extra == 'scene'", specifier = ">=2.13,<3" }, + { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32' and extra == 'server-worker'", specifier = ">=2.13,<3" }, + { name = "transformers", marker = "extra == 'all'", specifier = ">=5.14.1,<6" }, + { name = "transformers", marker = "extra == 'local-worker'", specifier = ">=5.14.1,<6" }, + { name = "transformers", marker = "extra == 'scene'", specifier = ">=5.14.1,<6" }, + { name = "transformers", marker = "extra == 'server-worker'", specifier = ">=5.14.1,<6" }, + { name = "typer", specifier = ">=0.27,<1" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.51,<0.52" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'server-worker'", specifier = ">=0.51,<0.52" }, +] +provides-extras = ["storage", "dialogue", "scene", "actor", "all", "local-worker", "mcp", "slm", "server", "server-worker", "test", "frontend", "benchmarks"] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { 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 = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, + { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" }, + { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" }, + { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" }, + { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" }, + { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" }, + { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" }, + { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, + { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +] From 3ff20ec9aebf4bea7efe041b0ae9ca2e637266c9 Mon Sep 17 00:00:00 2001 From: Talha Date: Fri, 31 Jul 2026 00:21:47 +0500 Subject: [PATCH 3/8] feat(desktop): add managed local application --- .gitattributes | 1 + .gitignore | 10 + desktop/CHANGELOG.md | 3 + desktop/package-lock.json | 232 + desktop/package.json | 20 + desktop/runtime-constraints.txt | 499 ++ desktop/runtime-manifest.json | 43 + desktop/rust-toolchain.toml | 3 + desktop/scripts/build-desktop.mjs | 32 + desktop/scripts/fetch-uv.ps1 | 59 + desktop/scripts/fetch-uv.sh | 36 + desktop/scripts/sync-branding.mjs | 10 + desktop/sidecars.json | 17 + desktop/src-tauri/Cargo.lock | 5337 ++++++++++++++++++++++ desktop/src-tauri/Cargo.toml | 30 + desktop/src-tauri/build.rs | 33 + desktop/src-tauri/capabilities/main.json | 9 + desktop/src-tauri/icons/128x128.png | Bin 0 -> 21616 bytes desktop/src-tauri/icons/128x128@2x.png | Bin 0 -> 79588 bytes desktop/src-tauri/icons/32x32.png | Bin 0 -> 1995 bytes desktop/src-tauri/icons/icon.icns | Bin 0 -> 1373347 bytes desktop/src-tauri/icons/icon.ico | Bin 0 -> 99450 bytes desktop/src-tauri/icons/icon.png | Bin 0 -> 244327 bytes desktop/src-tauri/nsis-hooks.nsh | 5 + desktop/src-tauri/src/lib.rs | 1700 +++++++ desktop/src-tauri/src/main.rs | 5 + desktop/src-tauri/tauri.conf.json | 50 + desktop/web/app.js | 188 + desktop/web/index.html | 73 + desktop/web/styles.css | 211 + 30 files changed, 8606 insertions(+) create mode 100644 .gitattributes create mode 100644 desktop/CHANGELOG.md create mode 100644 desktop/package-lock.json create mode 100644 desktop/package.json create mode 100644 desktop/runtime-constraints.txt create mode 100644 desktop/runtime-manifest.json create mode 100644 desktop/rust-toolchain.toml create mode 100644 desktop/scripts/build-desktop.mjs create mode 100644 desktop/scripts/fetch-uv.ps1 create mode 100644 desktop/scripts/fetch-uv.sh create mode 100644 desktop/scripts/sync-branding.mjs create mode 100644 desktop/sidecars.json create mode 100644 desktop/src-tauri/Cargo.lock create mode 100644 desktop/src-tauri/Cargo.toml create mode 100644 desktop/src-tauri/build.rs create mode 100644 desktop/src-tauri/capabilities/main.json create mode 100644 desktop/src-tauri/icons/128x128.png create mode 100644 desktop/src-tauri/icons/128x128@2x.png create mode 100644 desktop/src-tauri/icons/32x32.png create mode 100644 desktop/src-tauri/icons/icon.icns create mode 100644 desktop/src-tauri/icons/icon.ico create mode 100644 desktop/src-tauri/icons/icon.png create mode 100644 desktop/src-tauri/nsis-hooks.nsh create mode 100644 desktop/src-tauri/src/lib.rs create mode 100644 desktop/src-tauri/src/main.rs create mode 100644 desktop/src-tauri/tauri.conf.json create mode 100644 desktop/web/app.js create mode 100644 desktop/web/index.html create mode 100644 desktop/web/styles.css diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..64067ec --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +desktop/runtime-constraints.txt text eol=lf diff --git a/.gitignore b/.gitignore index ffcf0a1..55a6fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,16 @@ pnpm-debug.log* # compiled output /target +/desktop/node_modules/ +/desktop/src-tauri/target/ +/desktop/src-tauri/gen/ +/desktop/src-tauri/binaries/uv-* +/desktop/web/icon.png +/desktop/src-tauri/icons/android/ +/desktop/src-tauri/icons/ios/ +/desktop/src-tauri/icons/64x64.png +/desktop/src-tauri/icons/Square*.png +/desktop/src-tauri/icons/StoreLogo.png # dependencies /node_modules diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md new file mode 100644 index 0000000..15d6d11 --- /dev/null +++ b/desktop/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +Notable changes to the VidXP desktop application are recorded here. diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..b6972e3 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,232 @@ +{ + "name": "vidxp-desktop", + "version": "0.2.1-b.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vidxp-desktop", + "version": "0.2.1-b.1", + "devDependencies": { + "@tauri-apps/cli": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..e83472d --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,20 @@ +{ + "name": "vidxp-desktop", + "private": true, + "version": "0.2.1-b.1", + "type": "module", + "scripts": { + "tauri": "tauri", + "sync:branding": "node scripts/sync-branding.mjs", + "icons": "npm run sync:branding && tauri icon ../docs/images/logo.png --output src-tauri/icons", + "predesktop:dev": "npm run sync:branding", + "desktop:dev": "tauri dev", + "predesktop:build": "npm run icons", + "desktop:build": "node scripts/build-desktop.mjs", + "sidecar:windows": "powershell -ExecutionPolicy Bypass -File scripts/fetch-uv.ps1", + "sidecar:unix": "bash scripts/fetch-uv.sh" + }, + "devDependencies": { + "@tauri-apps/cli": "2.11.4" + } +} diff --git a/desktop/runtime-constraints.txt b/desktop/runtime-constraints.txt new file mode 100644 index 0000000..d564a7c --- /dev/null +++ b/desktop/runtime-constraints.txt @@ -0,0 +1,499 @@ +# This file was autogenerated by uv via the following command: +# uv export --frozen --extra local-worker --extra frontend --no-dev --no-emit-project --no-hashes --format requirements-txt --output-file desktop/runtime-constraints.txt +aiohappyeyeballs==2.7.1 + # via aiohttp +aiohttp==3.14.3 + # via kubernetes +aiosignal==1.4.0 + # via aiohttp +altair==6.2.2 + # via streamlit +annotated-doc==0.0.5 + # via typer +annotated-types==0.8.0 + # via pydantic +anyio==4.14.2 + # via + # httpx + # httpx2 + # openai + # starlette + # streamlit + # watchfiles +attrs==26.1.0 + # via + # aiohttp + # jsonschema + # referencing +av==18.0.0 + # via faster-whisper +bcrypt==5.0.0 + # via chromadb +blinker==1.9.0 + # via streamlit +build==1.5.0 + # via chromadb +certifi==2026.7.22 + # via + # httpcore + # httpx + # kubernetes + # requests +charset-normalizer==3.4.9 + # via requests +chromadb==1.5.9 + # via vidxp +click==8.4.2 + # via + # huggingface-hub + # streamlit + # uvicorn +colorama==0.4.6 ; os_name == 'nt' or sys_platform == 'win32' + # via + # build + # click + # tqdm + # typer +ctranslate2==4.8.1 + # via faster-whisper +dbos==2.28.0 + # via vidxp +distro==1.9.0 + # via openai +durationpy==0.10 + # via kubernetes +faster-whisper==1.2.1 + # via vidxp +filelock==3.32.0 + # via + # huggingface-hub + # torch + # vidxp +flatbuffers==25.12.19 + # via onnxruntime +frozenlist==1.8.0 + # via + # aiohttp + # aiosignal +fsspec==2026.7.0 + # via + # huggingface-hub + # torch +genai-prices==0.0.72 + # via pydantic-ai-slim +gitdb==4.0.12 + # via gitpython +gitpython==3.1.57 + # via streamlit +googleapis-common-protos==1.75.0 + # via opentelemetry-exporter-otlp-proto-grpc +greenlet==3.5.4 + # via sqlalchemy +griffelib==2.1.0 + # via pydantic-ai-slim +grpcio==1.83.0 + # via + # chromadb + # opentelemetry-exporter-otlp-proto-grpc +h11==0.16.0 + # via + # httpcore + # httpcore2 + # uvicorn +hf-xet==1.5.2 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' + # via huggingface-hub +httpcore==1.0.9 + # via httpx +httpcore2==2.9.1 + # via httpx2 +httptools==0.8.0 + # via + # streamlit + # uvicorn +httpx==0.28.1 + # via + # chromadb + # huggingface-hub + # openai + # pydantic-ai-slim + # pydantic-graph +httpx2==2.9.1 + # via genai-prices +huggingface-hub==1.25.1 + # via + # faster-whisper + # sentence-transformers + # tokenizers + # transformers + # vidxp +idna==3.18 + # via + # anyio + # httpx + # httpx2 + # requests + # yarl +importlib-resources==7.1.0 + # via chromadb +itsdangerous==2.2.0 + # via streamlit +jinja2==3.1.6 + # via + # altair + # pydeck + # torch +jiter==0.16.0 + # via openai +joblib==1.5.3 + # via scikit-learn +jsonschema==4.26.0 + # via + # altair + # chromadb +jsonschema-specifications==2025.9.1 + # via jsonschema +kubernetes==36.0.3 + # via chromadb +logfire-api==4.39.0 + # via pydantic-graph +markdown-it-py==4.2.0 + # via rich +markupsafe==3.0.3 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +mmh3==5.2.1 + # via chromadb +mpmath==1.3.0 + # via sympy +multidict==6.7.1 + # via + # aiohttp + # yarl +narwhals==2.24.0 + # via + # altair + # scikit-learn +networkx==3.6.1 + # via torch +numpy==2.4.6 ; python_full_version < '3.12' + # via + # chromadb + # ctranslate2 + # onnxruntime + # opencv-python-headless + # pandas + # pydeck + # scikit-learn + # scipy + # sentence-transformers + # streamlit + # transformers + # vidxp +numpy==2.5.1 ; python_full_version >= '3.12' + # via + # chromadb + # ctranslate2 + # onnxruntime + # opencv-python-headless + # pandas + # pydeck + # scikit-learn + # scipy + # sentence-transformers + # streamlit + # transformers + # vidxp +oauthlib==3.3.1 + # via requests-oauthlib +onnxruntime==1.28.0 + # via + # chromadb + # faster-whisper +openai==2.50.0 + # via pydantic-ai-slim +opencv-python-headless==5.0.0.93 + # via vidxp +opentelemetry-api==1.44.0 + # via + # chromadb + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # pydantic-ai-slim +opentelemetry-exporter-otlp-proto-common==1.44.0 + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-exporter-otlp-proto-grpc==1.44.0 + # via chromadb +opentelemetry-proto==1.44.0 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-grpc +opentelemetry-sdk==1.44.0 + # via + # chromadb + # opentelemetry-exporter-otlp-proto-grpc +opentelemetry-semantic-conventions==0.65b0 + # via opentelemetry-sdk +orjson==3.11.9 + # via chromadb +overrides==7.7.0 + # via chromadb +packaging==26.2 + # via + # altair + # build + # huggingface-hub + # onnxruntime + # pooch + # streamlit + # transformers + # vidxp +pandas==3.0.5 + # via streamlit +pillow==12.3.0 + # via + # streamlit + # vidxp +platformdirs==4.11.0 + # via + # pooch + # vidxp +pooch==1.9.0 + # via vidxp +propcache==0.5.2 + # via + # aiohttp + # yarl +protobuf==7.35.1 + # via + # googleapis-common-protos + # onnxruntime + # opentelemetry-proto + # streamlit +psutil==7.2.2 + # via vidxp +psycopg==3.3.4 + # via dbos +psycopg-binary==3.3.4 ; implementation_name != 'pypy' + # via psycopg +pyarrow==24.0.0 + # via streamlit +pybase64==1.4.3 + # via chromadb +pydantic==2.13.4 + # via + # chromadb + # genai-prices + # openai + # pydantic-ai-slim + # pydantic-graph + # pydantic-settings + # vidxp +pydantic-ai-slim==2.20.0 + # via vidxp +pydantic-core==2.46.4 + # via pydantic +pydantic-graph==2.20.0 + # via pydantic-ai-slim +pydantic-settings==2.14.2 + # via + # chromadb + # vidxp +pydeck==0.9.3 + # via streamlit +pygments==2.20.0 + # via rich +pypika==0.51.1 + # via chromadb +pyproject-hooks==1.2.0 + # via build +python-dateutil==2.9.0.post0 + # via + # dbos + # kubernetes + # pandas +python-dotenv==1.2.2 + # via + # pydantic-settings + # uvicorn +python-multipart==0.0.32 + # via streamlit +pyyaml==6.0.3 + # via + # chromadb + # ctranslate2 + # dbos + # huggingface-hub + # kubernetes + # transformers + # uvicorn +referencing==0.37.0 + # via + # jsonschema + # jsonschema-specifications +regex==2026.7.19 + # via + # tiktoken + # transformers +requests==2.34.2 + # via + # kubernetes + # pooch + # requests-oauthlib + # streamlit + # tiktoken +requests-oauthlib==2.0.0 + # via kubernetes +rich==15.0.0 + # via + # chromadb + # typer + # vidxp +rpds-py==2026.6.3 + # via + # jsonschema + # referencing +safetensors==0.8.0 + # via transformers +scikit-learn==1.9.0 + # via sentence-transformers +scipy==1.17.1 ; python_full_version < '3.12' + # via + # scikit-learn + # sentence-transformers +scipy==1.18.0 ; python_full_version >= '3.12' + # via + # scikit-learn + # sentence-transformers +sentence-transformers==5.6.1 + # via vidxp +setuptools==83.0.0 + # via + # ctranslate2 + # torch +shellingham==1.5.4 + # via typer +six==1.17.0 + # via + # kubernetes + # python-dateutil +smmap==5.0.3 + # via gitdb +sniffio==1.3.1 + # via openai +sqlalchemy==2.0.51 + # via + # dbos + # vidxp +starlette==1.3.1 + # via streamlit +streamlit==1.60.0 + # via vidxp +sympy==1.14.0 + # via torch +tenacity==9.1.4 + # via + # chromadb + # streamlit +threadpoolctl==3.6.0 + # via scikit-learn +tiktoken==0.13.0 + # via pydantic-ai-slim +tokenizers==0.22.2 + # via + # chromadb + # faster-whisper + # transformers +toml==0.10.2 + # via streamlit +torch==2.13.0 ; sys_platform != 'linux' and sys_platform != 'win32' + # via + # sentence-transformers + # vidxp +torch==2.13.0+cpu ; sys_platform == 'linux' or sys_platform == 'win32' + # via + # sentence-transformers + # vidxp +tqdm==4.70.0 + # via + # chromadb + # faster-whisper + # huggingface-hub + # openai + # sentence-transformers + # transformers +transformers==5.14.1 + # via + # sentence-transformers + # vidxp +truststore==0.10.4 + # via + # httpcore2 + # httpx2 +typer==0.27.0 + # via + # chromadb + # transformers + # typer-slim + # vidxp +typer-slim==0.24.0 + # via dbos +typing-extensions==4.16.0 + # via + # aiohttp + # aiosignal + # altair + # anyio + # chromadb + # grpcio + # httpx2 + # huggingface-hub + # openai + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # psycopg + # pydantic + # pydantic-core + # referencing + # sentence-transformers + # sqlalchemy + # starlette + # streamlit + # torch + # typing-inspection +typing-inspection==0.4.2 + # via + # pydantic + # pydantic-ai-slim + # pydantic-graph + # pydantic-settings +tzdata==2026.3 ; sys_platform == 'emscripten' or sys_platform == 'win32' + # via + # pandas + # psycopg +urllib3==2.7.0 + # via + # kubernetes + # requests +uvicorn==0.51.0 + # via + # chromadb + # streamlit +uvloop==0.22.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' + # via uvicorn +watchdog==6.0.0 ; sys_platform != 'darwin' + # via streamlit +watchfiles==1.2.0 + # via uvicorn +websocket-client==1.9.0 + # via kubernetes +websockets==16.1.1 + # via + # dbos + # streamlit + # uvicorn +yarl==1.24.5 + # via aiohttp diff --git a/desktop/runtime-manifest.json b/desktop/runtime-manifest.json new file mode 100644 index 0000000..306c32b --- /dev/null +++ b/desktop/runtime-manifest.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "desktop_version": "0.2.1-b.1", + "package_name": "vidxp", + "package_version": "0.2.1-b.1", + "dependency_index": "https://pypi.org/simple", + "dependency_constraints_sha256": "d3ed16906841952017f112903356bfe433a8515a37aa3f56e4af4dfa1f985835", + "python_version": "3.14.6", + "uv_version": "0.12.0", + "surfaces": { + "browser": { + "extra": "frontend", + "label": "Browser interface", + "description": "Installs the local browser interface. Leave this off for a processing-only runtime.", + "default": true + } + }, + "capabilities": { + "dialogue": { + "extra": "dialogue", + "modality": "dialogue", + "label": "Dialogue search" + }, + "scene": { + "extra": "scene", + "modality": "scene", + "label": "Visual scene search" + }, + "actor": { + "extra": "actor", + "modality": "actor", + "label": "Actor recognition" + } + }, + "media_runtime": { + "strategy": "system", + "executables": [ + "ffmpeg", + "ffprobe" + ], + "reason": "Startup checks verify the system FFmpeg runtime and offer a consented package-manager command; bundling remains disabled until target-specific codec licenses and provenance are recorded." + } +} diff --git a/desktop/rust-toolchain.toml b/desktop/rust-toolchain.toml new file mode 100644 index 0000000..7255511 --- /dev/null +++ b/desktop/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +profile = "minimal" diff --git a/desktop/scripts/build-desktop.mjs b/desktop/scripts/build-desktop.mjs new file mode 100644 index 0000000..4cc137a --- /dev/null +++ b/desktop/scripts/build-desktop.mjs @@ -0,0 +1,32 @@ +import { spawnSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const bundles = { + darwin: "dmg", + linux: "appimage", + win32: "nsis", +}; + +const bundle = bundles[process.platform]; +if (!bundle) { + throw new Error(`Unsupported desktop build platform: ${process.platform}`); +} + +const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const executable = resolve( + desktopRoot, + "node_modules", + ".bin", + process.platform === "win32" ? "tauri.cmd" : "tauri", +); +const result = spawnSync( + executable, + ["build", "--bundles", bundle, "--ci", "--no-sign", "--", "--locked"], + { shell: process.platform === "win32", stdio: "inherit" }, +); + +if (result.error) { + throw result.error; +} +process.exit(result.status ?? 1); diff --git a/desktop/scripts/fetch-uv.ps1 b/desktop/scripts/fetch-uv.ps1 new file mode 100644 index 0000000..1549a05 --- /dev/null +++ b/desktop/scripts/fetch-uv.ps1 @@ -0,0 +1,59 @@ +$ErrorActionPreference = "Stop" + +$scriptRoot = Split-Path -Parent $PSScriptRoot +$lock = Get-Content -Raw (Join-Path $scriptRoot "sidecars.json") | + ConvertFrom-Json +$version = $lock.uv_version +$architecture = $env:PROCESSOR_ARCHITECTURE +if ($architecture -ne "AMD64") { + throw "The first Windows desktop target supports x86-64 only (found $architecture)." +} + +$target = "x86_64-pc-windows-msvc" +$targetLock = $lock.targets.$target +$archiveName = $targetLock.archive +$lockedChecksum = $targetLock.sha256.ToLowerInvariant() +$releaseRoot = "https://github.com/astral-sh/uv/releases/download/$version" +$binaryDirectory = Join-Path $scriptRoot "src-tauri\binaries" +$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("vidxp-uv-" + [guid]::NewGuid()) +$resolvedTempBase = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) +$resolvedTemporaryRoot = [System.IO.Path]::GetFullPath($temporaryRoot) +if (-not $resolvedTemporaryRoot.StartsWith($resolvedTempBase, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to use a temporary directory outside the system temporary root." +} + +New-Item -ItemType Directory -Path $temporaryRoot | Out-Null +New-Item -ItemType Directory -Force -Path $binaryDirectory | Out-Null +try { + $archive = Join-Path $temporaryRoot $archiveName + Invoke-WebRequest -Uri "$releaseRoot/$archiveName" -OutFile $archive + $stream = [System.IO.File]::OpenRead($archive) + try { + $hasher = [System.Security.Cryptography.SHA256]::Create() + $actual = [System.BitConverter]::ToString( + $hasher.ComputeHash($stream) + ).Replace("-", "").ToLowerInvariant() + } + finally { + $stream.Dispose() + } + if ($actual -ne $lockedChecksum) { + throw "The uv archive checksum did not match desktop/sidecars.json." + } + + $expanded = Join-Path $temporaryRoot "expanded" + Expand-Archive -LiteralPath $archive -DestinationPath $expanded + $source = Get-ChildItem -LiteralPath $expanded -Recurse -File -Filter "uv.exe" | + Select-Object -First 1 + if ($null -eq $source) { + throw "The uv release archive did not contain uv.exe." + } + Copy-Item -LiteralPath $source.FullName -Destination ( + Join-Path $binaryDirectory "uv-$target.exe" + ) +} +finally { + if (Test-Path -LiteralPath $resolvedTemporaryRoot) { + Remove-Item -LiteralPath $resolvedTemporaryRoot -Recurse -Force + } +} diff --git a/desktop/scripts/fetch-uv.sh b/desktop/scripts/fetch-uv.sh new file mode 100644 index 0000000..e751452 --- /dev/null +++ b/desktop/scripts/fetch-uv.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +case "$(uname -s):$(uname -m)" in + Darwin:arm64) target="aarch64-apple-darwin" ;; + Linux:x86_64) target="x86_64-unknown-linux-gnu" ;; + *) + echo "The first Unix desktop targets are macOS arm64 and Linux x86-64." >&2 + exit 1 + ;; +esac + +script_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +version="$(node -p "require('${script_root}/sidecars.json').uv_version")" +archive_name="$(node -p "require('${script_root}/sidecars.json').targets['${target}'].archive")" +locked_checksum="$(node -p "require('${script_root}/sidecars.json').targets['${target}'].sha256")" +release_root="https://github.com/astral-sh/uv/releases/download/${version}" +binary_directory="${script_root}/src-tauri/binaries" +temporary_root="$(mktemp -d "${TMPDIR:-/tmp}/vidxp-uv.XXXXXXXX")" +trap 'rm -rf -- "$temporary_root"' EXIT + +mkdir -p "$binary_directory" +curl --fail --location --silent --show-error \ + "${release_root}/${archive_name}" \ + --output "${temporary_root}/${archive_name}" +if command -v sha256sum >/dev/null 2>&1; then + actual_checksum="$(sha256sum "${temporary_root}/${archive_name}" | awk '{print $1}')" +else + actual_checksum="$(shasum -a 256 "${temporary_root}/${archive_name}" | awk '{print $1}')" +fi +test "$actual_checksum" = "$locked_checksum" + +tar -xzf "${temporary_root}/${archive_name}" -C "$temporary_root" +uv_path="$(find "$temporary_root" -type f -name uv -print -quit)" +test -n "$uv_path" +install -m 0755 "$uv_path" "${binary_directory}/uv-${target}" diff --git a/desktop/scripts/sync-branding.mjs b/desktop/scripts/sync-branding.mjs new file mode 100644 index 0000000..218f170 --- /dev/null +++ b/desktop/scripts/sync-branding.mjs @@ -0,0 +1,10 @@ +import { copyFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const source = resolve(desktopRoot, "../docs/images/logo.png"); +const favicon = resolve(desktopRoot, "web/icon.png"); + +copyFileSync(source, favicon); +console.log("Synced the VidXP desktop favicon from the shared icon."); diff --git a/desktop/sidecars.json b/desktop/sidecars.json new file mode 100644 index 0000000..a16bb0b --- /dev/null +++ b/desktop/sidecars.json @@ -0,0 +1,17 @@ +{ + "uv_version": "0.12.0", + "targets": { + "x86_64-pc-windows-msvc": { + "archive": "uv-x86_64-pc-windows-msvc.zip", + "sha256": "68200e25de594df92387186bbfb9d9df606ec1d87efaa0ae0c7f690970e53db6" + }, + "aarch64-apple-darwin": { + "archive": "uv-aarch64-apple-darwin.tar.gz", + "sha256": "2b9e582af54f84fa50c115427451a6c13e80f43b52f8282b8af5791077317bbf" + }, + "x86_64-unknown-linux-gnu": { + "archive": "uv-x86_64-unknown-linux-gnu.tar.gz", + "sha256": "eaf842262aa1c418d8ecc5605f02ee1ebfd369124fa48548e85f9481a47831a9" + } + } +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..fc57db1 --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,5337 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic-write-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84790c55b5704b0d35130bf16a4ce22a8e70eb0ea773522557524d9a4852663d" +dependencies = [ + "nix", + "rand", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.19", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.19", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.19", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.19", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6792296e6f389268016c77db21ebae1fc0568f2fccf88b1ec7e2ea71330afb4c" +dependencies = [ + "android_logger", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.19", + "time", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.19", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.19", + "tokio", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.19", + "tokio", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vidxp-desktop" +version = "0.2.1-b.1" +dependencies = [ + "atomic-write-file", + "hex", + "log", + "serde", + "serde_json", + "sha2 0.11.0", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-log", + "tauri-plugin-opener", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "wait-timeout", +] + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.19", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow 0.7.15", +] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..9daecd0 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "vidxp-desktop" +version = "0.2.1-b.1" +description = "The VidXP desktop launcher and local runtime supervisor" +edition = "2024" +rust-version = "1.97" +license = "MIT" + +[lib] +name = "vidxp_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +serde_json = "1.0.151" +tauri-build = { version = "2.6.3", features = [] } + +[dependencies] +atomic-write-file = "0.3.0" +hex = "0.4.3" +log = "0.4.29" +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +sha2 = "0.11.0" +tauri = { version = "2.11.5", features = ["tray-icon"] } +tauri-plugin-log = "2.9.0" +tauri-plugin-dialog = "2.7.2" +tauri-plugin-opener = "2.5.4" +tauri-plugin-shell = "2.3.5" +tauri-plugin-single-instance = "2.4.3" +wait-timeout = "0.2.1" diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 0000000..ae6d059 --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,33 @@ +fn main() { + let manifest: serde_json::Value = + serde_json::from_slice(include_bytes!("../runtime-manifest.json")) + .expect("desktop/runtime-manifest.json must be valid JSON"); + let expected = manifest["uv_version"] + .as_str() + .expect("runtime manifest must contain uv_version"); + let target = std::env::var("TARGET").expect("Cargo must provide TARGET"); + let suffix = if target.contains("windows") { + ".exe" + } else { + "" + }; + let sidecar = std::path::PathBuf::from("binaries").join(format!("uv-{target}{suffix}")); + let output = std::process::Command::new(&sidecar) + .arg("--version") + .output() + .unwrap_or_else(|error| { + panic!( + "{} is missing or unusable; run the target sidecar fetch script: {error}", + sidecar.display() + ) + }); + let actual = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success() && actual.starts_with(&format!("uv {expected}")), + "{} reports {:?}; expected uv {}", + sidecar.display(), + actual.trim(), + expected + ); + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/main.json b/desktop/src-tauri/capabilities/main.json new file mode 100644 index 0000000..f3f4ca4 --- /dev/null +++ b/desktop/src-tauri/capabilities/main.json @@ -0,0 +1,9 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main-window", + "description": "The bundled setup interface; no shell or filesystem API is exposed.", + "windows": [ + "main" + ], + "permissions": [] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..07e4996a94e4068c63e27b1e3f931e3e2e45512f GIT binary patch literal 21616 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

IGO}*Fyz@2}Mx=fhuqTUjyW( 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ebab03861953002a9911719fafd3ef68390ae7d3 GIT binary patch literal 79588 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..ddcc71122152ecf598ab3c6c97c010c4b6db488b GIT binary patch literal 1373347 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

}rDxmC5cwIoi1&!{i? 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

!n}0w7Kx2i}eWvX{v3kkvBx1iH$TSS3Uo( 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

  • -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 z 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

    DpE; z3W$g+xfenOD@>9rm%;L^?avAF0SAEBV?@r(bDB8$(+PLQp1QmjB+GNpIal`&2#X{E zdqjh!z)|qMHkgaH;#?Pu!Dzia^Yk;yMcA=pdz-|yb#Ifx-L6$4Wv(Ax0NN4oJV10f zUfqc*lHZ{{X z$-*GZzs9sl@*J0ZV1w;b^*@taDT9CN!|I}HVEdi5sC`n%S0?xU^0FIE8R7G*RR{be zgnXJ$xr^UNaCM#AC6EQeB4oZ62QW4+{ON(M&EO(4QjrB0{J(>BDVtju8ypw2@{^VW zpqaQV2IlP*0hkQ4>H{mAVM!DUgK5ELOR$*Z#ourcBW z2pns=;Jp+xm;&m8C*fes>y^!~^u%H8RyIXIzD_&!G_A_ersugfUN)?sKkO0ir}l1V z-9@Vnj`M2}>?&;Mo7Js+?|1L|NjE*#0x&gb7!Wl=Fe};)Y7`yVM}nydW|1sY{nHW% z6ET7Cc8I{eN^XX7*;1wYWGRB8wE>guLlRQqx_{IwZ8vhlV zps5VX|8~l;P~GwwE|e(;X7O1k&ph+AwyS{cIWFMyFp+>d<&4wc8o-P-p(PO_vHzTq zSd}*+=YoLD zc;hu)T*i2Aim^R{Ebu752NqOmOJ(|ZZ%XzIkAOshwt*6QB2=HT+`Bozc1+8e3uQ9C zmnwawzS>5xGsu?jwpZo)N?HcbnN}wKTeW(P&VwU2>TkS0j1}>cW{V6HnqFji&C=`6 z;%9scJ$ibM>T6&IG=;mMFGdvo6cncy0^HD;X|eKst|8&GzTVKdVg&kk3t$%W@Pf z9TSY)93uwu&$FbWKmCj^Xi(eqtA~{jvmAK<=?Ek51FL>N3BVvHpl!4zfb!pw1e{

    TNrUot1VeztRX4p! ztwtvbaY0O47uByI1T96wj=W3cTY1ph1V+XaNIujk_A;rycwOFyYNTc3mm2;>B^`uF z0<6!b7W#R9F!*v@oGUgTHV?!_>{us$l1)wUg(dB!5AA=SMBI(~~D=bL|NzdL<&a2F{#e(E8uLI&N>`8&U=_0O81>V(h-Y6E$5U)jKE z`S3fcUex~H6*3?rp!BuB{N+n@j1!m z_X$UV^MwvW&cEqjPat>N2h7%kK=2tzX7s)EJWU3#cOZN&sLLXwc$^1NJe|%-{6g{; zgm2}6`5EgOV-88SeKRjK=oYm2Qp5LPTh|o#1<8fzx{W z(?qfcceGWE1CJtkb!%*cLEOAh2%H3aB%@(ZYz;B#|5dHy!F=U(WX8&kGB(-zC2J@A5IQ0JvyqOHENgIjoZ=JS{D%j#6ZwKN7{C*2|$+t zP!W?@5d6*2?>$*8@4xqc*mM$k;MUjMC1kpooaA!8`AAog!SpSR2|T}ON_-Z4l8vFY z9F1Zvn2a#;NM&Fauvy?w{;g=kBnStAj__S3j)s=-TIZg3Zgb5w*C=F2g3^(?l4b0g zp-`B^>Melhb12{YUZD2_iGK?DXZuS*0?Z~YO5Ffolp4i*W{(Z|4*pq?fZK1oUGF!lctrx# z+hBQsaMf4(E7d~}kfi;LGtX*{k-2n|76^<_%eWxPI>)Cv_T}qWNv7TWrd_{va$ZPe z&XJY?u>7rZK9JJ}c5f9vWltcOBG!fG2GbEFD9Wdi#EIe}NH^yp&eoTrGJ^1t%p57_ zkjK9vjC{Bgj33(i%Sp4Ywd!SEvd0}Nl{~%tcSOhwBd-r+ziWf|q=E?P`0_-rD`Y|? zKug;KF2FnQY>ATRuBXBo_(UsuoS-?hhxilte~UbiF<@O&agrcgZ%to(__jT|)ew*+{tTzoO0lP!|~D0^OR{P*RxRRqPzg&Dfh zwAF>dXCVO?X2!Jgl#LI{f#Xp%q^6LbsD-h>$vm1@cauZ#Ph{3p06!20{&~S zzp5<*p36EQ*uNr`Cjks@0QVkz5)e091Q!U>fPC)bk3HV}=(_8~P49LMkYT2LZA%=R zct6)?S8}f0^Hiz?B9Ml&1(gDs0D6D$!H0B;f%UH=dC>uJW~b4lz8A*Y+9DtxAw6=E zA_65k1kVXt@<9|8LtH6Mu=p9Ix&y^N`FA1?^!Q_sg*Kk-EkjdIRMM;=_NmVvdYFu3 z?ynjR5b`KO*2IaUY6~mwM{A?z$^HlIuL#QvaeBd?I~kg;g8!oqo*Nb2;|l};1Y|+> zobjkKNf4!i`8n5~=c#NoJS$CH-qO!%drO_*>sK8ks%D2iA0udnkY?|zqjs4n`oI`q zTZqpp#s~5OzpILYpEfe!G=rWY0-zu01NW=D*&W%uGOEmryL{FTXpUgZ*b{Iy&^Vip zj`b8|oWN{euPp!;1oN;!bD>A+GeeVLPdZVUlgyhe8ozaQ+)A9ZeDZj+(=?Q35XHNQ8T8>QdPyg%YjjQ2s} zLxB&e*>8U9TMAeILgxMNyz>s_1N@i&`rj1(zw?gUo4fA3vpMeA zAGluw;J!0{J{iQs_wo zg9F(=C!7|YQjm40PJO;d{mK+E^V1awyd!NTZfs6EeTEIEt^RthHvATiL{;2U!!9oD1qDG4WA|VM*>fzDup&gsJy;*2kz=r^dd`{Xk zBoQbpS{2_;*cGF?Af6;hpg|-NZ_9DR>bIhzYF5>BQ%CD%BoOi=$BFGUdx?*GOVbh< zhZdiY;yjf$m-nsktQGO`F;2fH6Yo1SiFD+?j1VqS1S}MXjDhWSYJ_BLvXr@EB!a#&`qo}}m zG}ox#{8jGk>t0jv^Rr6i+MK?hBYF4!qk~>^zJ~h)sdoCFY;w*C;J#<_GL}}T8;%`B0Lq4hVdvoe%`y2bwsO+WZ)D*!06NSc{WL#;SHS(SXQ(#C>8Eh z5+Q8>CM%eiXRHqg#+9fC;4;&%?lY*#5O(EKtdKbIp6DoS5jZ~`S?*u=PW_SfexeW; z_ff0f*LDBO3z$OKCsNu1(mB#`LQoD0Bpm*Bn6`-u`jM`Q{kf0;s9(2iE(CvO{AmZY(z!99vjMSzuo<#3 zzA7XhxBzTOK!VmQwi#SrSIskI;lK6fTfzA*orCvXAmzQ*3;P-dHVgejd;e5+qBt*; z#9Q3Nh?+tkJJlE``G5fpl#4(K3(_k5eBz+{g3LtXLIMC}YH)uO=f`>e$zm0Hs`No2 z{B%l+`2<6C${^$X%o+mn)3t-jak-lD&fvIL$rqSIlxIrS?q}v3eFwvy6JmKY7na)7 zM6jk22&aw&R+1j>BkqCi+ji(?{iO>o6d|0mrNVtg0`r3^HG8!n0hrv}Cx$gos@&}t zn?L{dw^bSNrW5Wgcl0hsLfN{awRK=k5+j{gZdZg}GjH)t>Xa!DXA7TaHf z&Ruui+5GIM*Gt0ju)^x2b~>l*4W}bzO(uv%atzFmd^ZF~CLAuf@Iq}zY>`U#*HWz` zwc8qA`C%Do-xTDuI6a|UB2_*-O2x&{&*ybtwV8X?S(F54oscLoV-M}U#UXj1^}!y(7~@VsHB^rv^T|j^(NSCsloeG&-j@j~or)@;yCL+loTyEE_P%h| zbm_(y@qv2q_Z0*^Bb)p1)bu8kpRb4zP|fHj3C2BgUxn;fLJxi@P!6;f{2z^=&y@M& zigBr_do&dhrlztdHq{|9-;`irakuz(VJHzuMtTmCktQm3;5djmrgUc>=qu4Zkf2Ti zz)i2DxHp4A$HHhKynKY~*eoF!kukG?&`~!T%<6_spKfhn^RrmDs&BY2HhrIE#jQU^ zb#}e1x?SsIKym~0$D%YfvO*iQBQ?tTVbg@*1dQ4wYJUM%+*{`TFKy2M;(2x6&}vl}895cWYbO8XTNeA+02K(O0M6ps~O03dxtgFpv1 zi8npH^1uAc@5=L!*UFnDfc19y6<6vwAr&!gg2x0k;59FjB;W`cKLbVh;SYYG22@nb zwBoo2E%0`#mqk%gOp{EaK@fj}N|4CFKY$QNFP`-bSjh`TVvS)xIzAby- z4s}o+H^uJM`8LF=nSBB&X}&-ZXg~1WRLrOciMxK5p9V^wZvrln3+@$_aaVC&g5Amq zwwYuRX5A#%0hfBD0RgDR^LZ=$-NcIXe~e0hb4!*E$U^pm|1@OC%{`Z`y*x;AvLP|* z_IhNy2a;Szh-3310f4llH!?69eW92^Hwgl;0W~za@qy4l7FMF6j`>wY*XhPQ%UD>t zu}OBkbLY;y>Z>fTs&tOs_|9iS8!ke+C|QB$(p)Kmzy^nd=8JLO1_f5e#DH9zFn7KI3TD3YP#z z`;#&vc;7wu>KdbMaDfO30iWM^;|K=|fjAsfQpr*BNc0Lz}L+D}$AiB(n1$&@|C z=ly7^WRL_t2T2Um3Va?A045Vi=+(+Q+GbBfqUes2`DBT#-`5HY@41J6GDjN0KU z??!?*kKMZML)`~Xk;j0Xu;0*z;CC9t{bV4C56B+;9jtmm18^}l^rBVgtFEce3~c}X z572MOc)t}QrcOPr$<_JN+s3>U_7XX7k^sCNKrU2~aNG74^~-!&5|owSf%fdTe<(QM zFQ5s)B!r&35_KuOGn(8oEKkvEMWLUU30w=}1M7p&Hwrw!QIc8I#`IhQ`%l#cNL>Vn zl831(W2+ewm`A1}8a{{atB_~5K~_}Yece>Ve#cl0p6KS$ zZR0r_^dk{UXKrOvpiOX4Yy+_1fzXWHmx}GsM`YZI$B|ULsr^6^7#c0KXoM|k6AD3< zkJo3^ij6e9ryH1SYxa;85A15sRU}=hDuYtBKsq%Y3gl*Pu%cE(;gJ#HaIcEHhWPu=m^UXIkw}=EVRri1U@Bdw4c&cB7fI!Fy_LoZa%sok);NgcJR=6A_ z0Ud$og|PPn@esK2rW>2hLgLfPGeYBYPe;UQ8=l9yq@qQ&5|Y~{;WZ$16?6zzTJl}f zGn#4&t&Gku12m0ua+D-37himdvfepn_7qj?RL;OoO+59JMxHxhjppIG|3?6!=Lo20YL zuH{bVpk|I1x{es`TC#OQQq~qgwnl1G03T89&i_Xt{>}Tj`;x7*=wE(%rL{^jmEBZJ zQ3;SN8fn{m#jo7!%rgL!*VX)n<&bxm z;-;UveDWRLG%C0^ju$mQjDV=fZjujCd+6FgB|DT>RWWld~^%@Px56E-?tpcjmugI7k6*KGNW|=sEFYv+(&+9}E z2*YH67+AaZCVOq}pNt=3@N<>(qGge1X+`c^^XgxE|ScNk8&-sUuPY{1s{7X!!(e|oL zCnFI7Bhg3A50SEi)yc6@CB~o%IH}j#T}i8Z*M{}MJPaNH?@O|z`$*JFDbEcpR=zlw z7=mFEM5_lxRAqr(vWJz3nD04exN@;(sgCO${dy_8Z)V8;)p=^h14w8GSpwizp#hM4 zkWE+e^YbJDt4#z}wJA*L8xDQ1=CMyz)Q&m%Tr@`^!%-0GNoV=f>*0DHgTW3KwY0^xYw|IkHUpl-*By4u{b_f?c`waLXWm!>}*5y)&SdOtHxhub3o7y=-qH(Eh| z^;iE{wdZlZL+>ATKvV_K7t3Ls>~8txFPlvdZ&D=SY6$Cq7x zxtd5ZL(e3@z4F>5#$UbWT6vB0m6LMIFK^aw(5a3y;xaCPuewJjV~!YH?bxo$pCm0; z3L%lC;DHD3Q_TXKcyUoQY+3@_Lhr8Ay+Aw(a_q?`e_qA6o_ylTcCRpkEv$YfkVvpW zQb0oZ+{RH#4ZcjSnKk@bK>$bqG>FT-!uo{FXUJ{Y3JB{mXx{IsFwcWDor zgyGAQOpx@_PWai+eyTRL1g5pxK3kfRT;L23^39 z0px*J189Kq9NegKY@_&D!+0`NPXuIC`7mw@`;u?X%}U)7AJrInUs?cE^6Uw;PkD`C zf;Orqj&>kC7l%k%2B0J$qOPzLFB#vyLc~qga`TpY;_sW!_tskR@nSkRFCT5vL`^(ZUA0x5<*BbMvEYcm4I(Yab1W z5%)TXM2uQ%m2 zNjiLl3sa#X{stt-HwnN8?{{N}z8I-Pk;26ag1?ZYEH5q9H}j&(Wwa_x8?24JyMDD0 z#t%b+0kjWcClvr9`9a|g=#+EP>J`i()0e0pR0)}W;6`#&{X+emDq^#G3r(GcKjb)4 z#TuK{2md+x)_#T;z%W5^j`ciKaj*xAww?hX0iZxQ0iq9pG9W}gH1Dsu_8JX@K=vdC z1iG(%?d#16C!MG^y{tWa0nR=5T%8Vply`?r0l*1hy^t(?MGywQ?v#Lhzp&~_rr{Le zQNVq7^UXJFJ7BZg{RXD{sG?3cz>rjwiy-ut5aL)z@Bz>}K&PMS12{frjm!G(s`A;! zN(6Z-d_EWZ4M_k3b6pkosh9S5&}jEVdbZ~C%%#pPojSaNgs&|g$}6Z+cHWScu?a3{!36d z4c;qz>-mN}tsH>SsA;m#I^@4Gc+NZazfYpQ>8cNF|Mfe2zP1H<15w+7>``R_`c=U6 zxvY5K{@2aw`&*K+D;oZ@?0Tn}jg&L{HRGS5F(_YAT`Q$8`xXDNPrxL5o!Yonf8m(2 z9yjt%wWr*y8xs&E0qqpY)L{gp9ZqkHj2wavfp#24QCLJ|-Y9C}gH8{PMNN?8G2`gY zrLvz2du`OmkK)+iTk?4A17hPD19@=AYBaH6hM3IyEm>G>HbyQ?NnVra=C?+U8`d$c z3ZTr?&)e5*bFGJ^#~F=KT+@@=Q`=rR0P8h0KPQ% z%wYervhJFARsDvPd^HfX(yAr*HK-T^Rpoq>#M4)F0ZJI&`^r>dd4ErDk#*Ornc60p z)n;o3`~m93!1DagXyXk8PA<3VN60@}FNe!|AmL?NWNUQe!^2)>e+9m$&;RsOPwMC? zQ2QyTd|r%yo>a0QYvuys2wZyUWfEBS(H{Iw64;j|Xy8L|^;K7E;JNMATVz7uMokVb zy79mS7txRa(4L@*M^k`*gIH|Y z@^+#KW^+TVK zScAyS3EIM9=Ko322jIRZfa}Pg1bfyT_bSr@%=jO8;K7m@Jf=|o|7PyJ+hse7G~boA z_c8L`d+)u3yg(qqGz~q|eeajJ(@%eg{vdb$zw_J~x~HK50S$5>Bq8s;N8TQpWA?7N z@r%q{D>GN-s;Yes%-uW(NoSW?wJI|s;*B@n7o>@?!8^Wxr|q{t>dgNiANYe+t0WA! zX}~AZg+D-BfyY2DBnjXX+@r5?*ImCfEq>Y)J9g~s$*RkIalZ1MTQ8jc_%*6($$n%^ zj#0kPjFTe?TfWUOjYdBFi88;m0@yd46HH0K^P!+HZ4pfnC=&R0ctszYIL}59=c#{h zkog!_JW1P;BwL(QkO;e?k6+q?M=n!mt;`cmQ-GucRH8F}&}_upi9UALD$(-|ax-Q} zTp!#6JU`-DC6)Ch;>n{VD%@L&BCdfpXKs_;8i%bYe#&x0yK!OKO}>{TG)O7Xzj>F8 z0d8?_Ulv&X{?!!w_LD3y;sUf4%GxcMl8n`L(<6RTk@oK?@%cY!6sB7RGu{r~m9 z{!KUb%iRyOx&UgVf@bppZ=se&fPVDRM{UGrEo9Ykp~kscSkN4D%skUhAfulJ&GIf( z_G=oDPgGG8xBz6?NMS}2U3|TGND)w(&F7lG{_1P<+>SE6Jq{K6mQ*8DB^2W>WAH_N12u{4Wa~a5m8+$iQbcKTp7JNw-S*2l9P>j~b#_wM+f zc~M{m%oG88f>zue3j0H#dq4?nZNIypc^OmgtcU;d(d z`<=IgAXh;Q?3(t7LvpOB9M!QI@0@aqsdK)p?FtBvcp^}#_Xy+&RT|IW&x7^{%zIh^ z6b&FG@Esuff;fPjoTU5%0`kXtM({f(foRi>MpMHN(>&bsQ%*U}V0C8tKM1Y5xfBW^ zF46y<6+Q~jQ@~@k<&8_BMnRKTnSBD2iKWC43I+etaq*P?=tVPLsu%tt^ z1o%OZfj?d@c{a}LMT&vI5vQ*kNdYE-#$_$}vD;;3pEl~D_!2fo>94ey;qM9y-<+G6?^_t_t#lQSG~W{ZrFTXKGHpIGr0C>k}N8T+F~6&c|{i=0Sm*97j<*}N{L4F z{V}k?l$k z@n6aEu$+H0w<~c-S2*Dj+?Xx5dLA56kFN{wAz2>nWwB0W|RxOtEm?+J=W8;T23NV6gd~TQ%+~Jen@F96e$9sH2Yx^1OSEl*fC{ z6F!h6{<^Wd9*Zf850C&70LB|PqaR9R{Y7Ij5(m&zUW4<^rWX>VbE+NX3ObIAD{(ie z+9j2ZpTCwA()~r;_ET50TK|5D!0uKZ75v|o1myk80@ADqpwuexezYU4{-mo{N-nQL zWI-~Lk6pDwK$?hj_QlaVd{kk5pnaB9nRG?*I7mdT5Yng5ZT0%D_*Vlr-Bm#XR<3gg6P|H-}Js@!+Yir5@ zn1bzH>JQwo_-#C2rbu{G1i=Gs=93YHCUnT9(A>@||1o4guDDVIhJtx5Q@zy+5wU

    ;kf3St5vS^QVVJ}!|uFZHiSS%WTon2!sGpl7P6bJmm9#cW$f}7|DP9J+H~eeq9eKt~LH}aGk4( z6Z6|VeqT@}t5P-GCfAwnp0Zw*`#YJulDM2GKkEuhFSXr%bPV2vwk0b_~Gt7O?U~=tQq_P zSldX~KK}S)762~P-uTV8-e!I9mtG9AxtesMlE--9g^JMOH-P#dNy0@JU7}-vw_9NT zv+m7bYu_Kp96_P)Dc^tq%ShWj_uOr=!JrVVTlP=tWGR(aNIp03EHG28-g4Wm#(BZE z_z@*2nkt@oXw2(Nv#_|1NCSyAfs=&jV6C3f;(tzshk)R@Z;To_QA zjB|}%Ds)zG2)gx|D|thJGqKUEc*i3{WLZ3L2L$Oj4GV9PT;uwiYrub z=SB<4cWWzvYMQl9%MHjM0`8wQX@EmOP`dHP8>~%0TL6Qgdo-Xkc7`l4BZjEqz3{>d zsv>xoRp+KDKoX$SJS4903LaKV;xAOof_-B9UAoL{r*qql~qh~Ri=ej{~jKYa?lKaMUqFIzQh@$b9 zb6uQQkTkhYct#tE!>CBvO78NO#ELO=)g!2$oDU({S{=nW-ZHMB%d`hvjZZLPIeg&k zd#BizbIDz?q^*bEwoA?Q(k3iS>mz^VwN^(NGp+P4W5cpZ7!i6xdxk7{7|3MdUwpC4 zGuXrUN`$KTSI9At0bvCj0-%S8%>pxG6apU*8f^d$B2)5G1~P|U97T$p02Vyt7YIOX zAYe|L1zumbgC>4-dSSr&^itWJ%;y5>fg+^BAd$F79}vP4E?Wrupy|E0hvi*Ymf49? zMqZ(IL)LB_0KFgFv(HpmgnJZ!2>v7igW$&s20sgdXFvE9aAt6yj@2`R-;)4vz5N8iC^Ghjcs7;?B0(=7^z$=? z@{LR!M8HO^hamUMwU9I%rrZPGhqD4lL9+&SU4QmSF>h}IX`8;y3bF!Xz?n4kB~4tr*3hS-3Sn{HqrJsPl+jNRrt*(6m~6C7nfKYQ!G5|CSj>24b0`6= zQL(?GJb)+)Vk3dU84YVq^w2pBUep8tSd;2~7TEoF6eyA zz>o!PL7?l+>oMK~`EV^5n4}+gGmH`O+8)OH(#V7T=%CaCP}CxqDtE?@L?Mduxh5Yl zmAQWI#~EjyVb{MA#)#@_R|(uGHDccQ@v>qeyIespEk4rd(67n5F7;*!_dsN8XxgIL zep!W^Ad5gZH=eB)so;*%68)_ARiepGK%q#6&pq$l?usj}RMkIxHvVA2j^z7jU8@&g zbg`jGv;pAcuv!V&7hZU=>0q$mFcV_D_T-aJv4DPs248y35d3%^KS>saC@5H)Mw& zRM}vlb2tK64AaJg#0yv8DMb*t2HIOB%~5Oo5~}S-vEqp{3)F&Z zACUl8wegTltIKWCg;$nv&7}A0#{gTgc|!2V9YCpGw?E^3%X)yX24GI~AEhRe;U+7z znut$pbgOml`7BjobPcp9jzQ#O-0#|JuQ9?4aNfFUZYSX&YFeU*K5$l;u-j5&h+i zqXocsfrfyQrQK{w7h}9t+}P`*N@lIwD%Ij<6%|Mw0r`3rt#GpiqkdC=H#G?$Q!0Xs z(Enu={#NcDEP&;wTLCpw9%+)W45scCj6y&afZExRpDGO4+XAPp4Ia-dupUfQC2T4VB{pFs7mlV| zQ*Ub$T6&C;|IiI)CN|=yhiIJtkJi8N9e5u`CS@4)7|-{oHk&f)L=+PyF82?XGL@*c zlO)$=Z*TrV^2?PyJvX5NlH^WT^yx+=7ZQBgW6x_aXZ(zP_bp8TKpRM^K@Q*pP(_3G!51JvGJQZR0AGuJ zf$5%42BhS=vF)}e{E5k5pQEje_tZWdoe=g25PBr%rcC6T5+nC(uh{`_Gd~T+7~j!$ z0!Re`9Lrqp8)p~X34ip>Mxr!53aHn$-CRwNGBkliiy+QaQ1oM|>8hB!AMMYzH9Ko) zcTkPfm+xcA2=xu#3Hmv?r-nRk_XPpxj-LvuP2dhYcOH352YcQUXP8+pFR<6?jd|Mp zM_O(3MR0%O-II;#2V~d{$f^EWUmLXmmI5#v8-68@!U3cJEE)Wl_zWwz8fw**TT)HU zEvT@h56~>kr!{CJ7;>@_@PPBltmBwmO-r5)K>$Vz;Puzvu%07qW{?1a`!i2JW1A#a z*HIh$7@grgK}kF;7^-$$zsxsXH!{JSLxN3x`Wa^$(TRhQs>sZSOf1M}__QB51UTf5 z))`$k3h##sSsax~GwIDB2g>K7Psrw#kf*M~L%2?7xHO*h}7NyWEpUm@o6kdp3j`U7r{vfYcgDsh6q z5I@~(Brxn-5I=Z2O!Ray*iLx_gpA3^0B=B$zsZXJ@tHsldEHO-vvN+Dsgd3vxDovE zMr5%+?ncHy1Y^)bDsrC3YZJsNPNHOUZS2(&mbsVbJp@(1nV0=${4suqRr{{FUndZq zBbkrahcLO%&I^|%EO`wg)^=DAEdc%rg2KJ^NkEs4Ai5RycU6FN5o@sKwv>SiG8!|S z`4*}#&TAES#SExBK=A*T?0;oK+kctrWYIWOODCl@%~UdI9&6U9+(t zz)Dm%JZ2^T=|BCz4i4ad7ZzzvUJ|k${r6X2eWiQy$tMi@MlFy&Jnmqxz51GMOs`z! zY~viM2MuaxK!hAtObr0TA=1MYQ!yW;q?t36{Db-yUn7sBQlV#WPP;D?}R|TJ&XIeCy#K{g#INY98)Tm!jYs;&s>kzDW| zBmo>4yl-EDr{k9y;c(uMH@7?g(T( ztwKK_bp!+|L=d&>wM{@EBbdMQ_B*iBm#Es z1Wk@b*<>8IrD7)m;F`bGaXJi$fYguD0)WH@y?iel?vfUm6p;Gd&L%{to0?dvBHgInM$ zAX65)2UPP<4r~b}%gB^YCK5~Pycgsv78MKXU4`h79c6(5+nsU7S+*Glh-)l0OGU6J zFmPcgli>m((tkvn^cD5%Mv#QwoK+C(YoUMTjM>X!!|$kQH~>*0i;JFw;ULYHPgdg3 ziFzQyO@QP?khSq7H}nv>j>C>P%zB{^s(=U~BJqCxJh<;iS^tqlfoq^;@l|lxay5so zpaK@ngEoSuABC47QXyq%2A18xaI6 zR=vvx>@ANPZecXBA#YY=Qdd%>yyLg3jzGa);N3dNuBIrRQH%;kG6yY-2f_TOokp) zks!D%muauvBLt_Jc@3j?&uKf)kI)^`2g7B4;s`K$1GL1L{NWEs9E3PCNRnwzORUN_ z0ypRkMF!c6_zv)Mv=RhC1K=J-Rl+bbE&ad zUD>ExngpL}-NeAP*=0(AQIMc$uQaiGsRaM3Rd9TXtp@*9AjleVsFip%-2q3mpxp6q ztW?%zA{SCTz9cre1V*ZH6ucJonr*`O-ha=ae-<a*Cn zeGaa9W8X$Ufso#B^D1XkhS^@Af2s@mbuyplbIGl&weibzo#cmD;u_dZaK9eMsU2+Q!)hX_D97uyNw9 z$ybT?XQB9CY*Vz&(GrSIH?|4#h_rYO282_a1;rigxv52*%(FF8wF8upPnE4d^rv)BH5HQT(wA^_X5l)H%+ie~ zK&0qlOQpA6N-wubu@y;b=O)Rm)z+3<+;ZD5vS6vtFBO%S2l<4&$+9vzDECeZGeYW-h;yC?a8y z_d+8eZ3)!wf0Wkc&QfuMyn!zA2=+3b0qjxQg3;e2v%->rA1gLSyw}c_s1M>>f_n^Y zi}Jxp1=}9&X5#{=(Cnx9*{Q7nUc)#T!JsFb%1*vVJ2Mqk^t%3s+TVd3?E3PHiavk7 zDdxdZl;n;x3;q#p02cy$%VVMoE!u+u2Y_qXoNpxJOm0kh4YzU#x4A%ZQHKs{e~ppa zB`SYcx35FTpgsoJ;sT6LmPLL1h$s~Oi~~ORFMknARXLo$Dj?h*L#)gY)E}qs2@Fs0 zJ=~bUS6DRxbAK5ZpOb3=RyiILQN$Ae9D> zharsE3Gf)%HBcVO0tvu5XP;w#_kkIvc=^p}H5y?!*ufldi~w@DAf&=;rStpqATP>m zZ4V|T^zVCUza)n1fxzRaIyj_wAItR9fF4GR_Jst1Wrzq%VGA)VGaQ4f^ zK&Kr6r&iVSU|P+W!cA&WNb&hyPgs@7dv+kmf;IAsgC=l3 zrIkK>0A3GcAhbDv!yyX-`Low(0WdOng&Gs_9uF&50KNddZ`0&(vyq!-c|&96%6B)SSIPKVOueA#ufZ!X_PghjV*~ro2 z^Tjg@1YmQ%Ir5N6#l3PZ!!-GE<>qvCGpyxd|Ar5bvoB410JEu9+Q<|5)y9IbC0+G3aztXdJOU*fheH4; zoGc%eJ_7Fc(BDeQDCw0%r7bEG2!4#H&^7>GCm7Pp#~=uZ|1f3DLI%8B#f5+^BXmX} zh0Jq}9taSG=nK;0kE~~p0Bo2k&_&wEgfrk(0bjR~K&$3WphS#K%k{&A-lo?;j}%3~ zOmHNr?%BlX3!nx_q61pswYk@N?WNfBqe3BB;aYaZ8Qhq$2PA^T21t+BpgK6_*kcSP zjQ!2HNo(tC<1zXtb;qOzRbY zw@_@W)t{^qaHV|!R+Exs>~B6Xj$0)|c6m#%OKf;)GaV8XGf7q%k%f$B61s?WY4BrK zpOyf+6a0><*Q#vDr@CKA0uIw8^torBv&7r_?Ybuk=vS(;Ah0}SLA)#0g5>Xc>REws z4*~VDM<22GCE=j*yz@`{ckE>zPD=)ud)oU-gJwcp~Upjk)z|F`a`-wKiJIZ6Q zK;~S-2c>8Yn5w8xsyQ>xR@4I0FNR@^_Hv$}&WHP<>Fk&4eA4wuAm5o)@|s9+j!{^c z9r^lf+=4Ao9`=9K|0fuO{#k%8_EWm1V|$vU!^FWhy#nOmE-i^w_VAvDI_UfG9111Q zyFGi;c4Xojn}-zV`Bdrn@R*-kj`A%@z#WXSLyQgxzhv!!nePEC^a2o(DX}{D*SGgQ zFvKg71Mz|~UGGn^5@lQ(#)FC=3;YcM53&(`HRkR;q@A*1WkiI5g}t!YRy{(L6 zL`U%o z87yj|?+xl*`w|^)vkUI#VXk*~@A?IT!noGv=6sTgUSA_Msi2i?^rlBnmK6*WX$JrO z(b8Yh(tFDrM;4$Qllx2-%cIHuoE*|N?J}X9?S4{)L>qw{_LP^wc7s&5wg`?;Y1vOc z{=^dN*l%a;Uw{1#`uh{r#q&?r(%tuyvF>F*Hg zA`yotfc^%Ko9F}Fpc4Z??-UG73h@|o?{l+p_3mmUIA1-J1fbzhMeDUgKI*M}o9jJ^ zY_tX0gR~mqE>K0=NaR7@ZI92%>qX*nmHHL}J1u~Ng04TFBy*9_@~rbhVh|_M;ydCy zp&&rmC$>v#R_%jtN@w5Cj!*UR}#A=exGSo9!)A<;2M^)Bjn zOBm6^ru4dNud~e#l=-^$=3y>JSzZ=2rXetq*-Y>SFeBkW11jV91YZ7mzFr@R#)~h! zs7&T3y$DbkgW^&n;SQt#!qHoAy=`NOe4d>;O%T6|D6e^>_9G!op&uTn3%r}yP@P5H z0uB~F7Zn#!YgGP<-!p7p5CD3Q97LcIpdluPGOxL*wJe+Gxl&)I@m&=Ck~;%5T>}d( zJ{k2nCLUj@UZ7fC;M@WOQdvR`)bCFY zO@b4FBJyVT1%fI38LD>BwKG-m?`<8AeMyt}DAmiH`T6?0<4!ovv@@9RC-MF&*y}Z+v^yNttTJah)n))|Gbe}!l5fB`{Fsps3$VyQ9NuOuEiDY zifr^YsF6miWScw)x&^&YNp(MT60XNpU12AzeMgqNZlK$V=LMHXP$a;Ok&%^yG^ObJ{7eh&l*ix(jiAU)sHTelt?UngWGq=E6nepW3($@)1= zxXxKHRWXv39xf;Ya&1fv@L6{3*xrA493E^uCIP4-U;vxSpu(t;pAaoTIT$z~&^70S zn+#`)00dGk$p~!-p2OogB!Ot>y=}3}TQsGzejoJRq{(bofw3nYwWQLIEb>B`u;@*D zNUQK_rnD_aP{qJ3?3)^#&j%W}zrR7MGN(q@8a*Sgd}UYkG6o71D` zz-M&NuW{e|Jx{CO(@#Iss&qIu5Mg6a9xl%b$De44D6KH21-=NRSRiPUX_9Wd0-k!x z8xQeA@_PNZZqT;eHP#k{EC`_%gb)HB)(aEK5)XC5*JNc{v1t?#c3j*y9|2R~4s-5|17x z`}*szECV*r>O&4q)V4P5uwp}UNTFj6EKdLM!3Q6-YMFzG_u(3N@9VC+-e!z(AcUmk z2|3OdI-l4eKSc}R6p$c1uZ(5f%SdDliqlQ8sqcHrsa}GY#N@>nU+`9+Y`75dOwrF0 zfTJwo;qZ$D#qELElPU)S*&Thc4@SoymmPyirBw>6Pz5nvPY- z9xlyp^9Oj#$ZZBu>x&}tL*i`}@O$stG)@e_{<+7hLg1yg&uN=k&)#_=A#?OMD8rxq ziN!9O0;p|*%7HAP55V);7Y{!8N9)(KrrB5QW%ePWI}hpa-g@iJp8VP?`&G%4&aQ>W z9;ONP@yDHD`van4)EY_6&!nNhU}qP>d&hR$o3t@`end;)Bv}iwCjqwm*^ncx6{^X< zPXJ^v2CLF?qIQZilqQ<}Y+5BRImc5PO0FR*;k@9CG(=3GT_phMOe}{iVDBmY>JZNPx|3CxLG=MZ%HBahZ|Je8HID*qXJgSwa?7 zM+Ui$Y(q0Kg{rNoG%LJmwUa@|xwGK?DO&g;KvIcs4-k0YFmlCs)_j1$BJ|Bd_N?4P z!seVOnR{o|^L5u7|!V?8lM_@n%DYQ0S&ZJq&~b^)sp{ zvUr2$l6%GGq$=WZpb@*2a73jaNr3U&_h^A%Df5V5+|2 zb@Zty3Gj>qQqdCRX@^DRj5P=O7A^pg{k!^m*0Qg}H%u>pV9wq^FbC;eBfsf7r2=P? zfPhW{0Z-ty*Iu=xmEg>K(hdL(d`J^^5Cqmd??c<+Ti4%c2?#R6Oz*s=V~3z%>k;)z z8SSa_W9N18VcG-NbLOxoVeWq!0*F(|{L%_o#zcittW6zlvs_`;^(4S4^Bq}YEO39b9k)ojaK9{ow={a#>IE-3 z8MvYVq0exG@sC!syFTA#6)WR8ex^RWcdh!b9Eh=p6@zde)Pg|}o07#!zwc(%7>}Cd zz=tFOQ3{m5r!PsL(f9&7#`mly0c;YI3;$jls`Pi1S(no1q<8t2juAyD&Nh->Kkz5L zLC8#?6Ttga8NP?M)*pg=W|Q>P=Tkud9a0HIgd>jI(2Mon0W+(jKmcH8XO&LuD|UXm zf+-5#Yd)Vy(3&O~?cl!*FJ(%VPGtYb>Q=Sztug5LYcz7dbo^yGimmKFnQ=*8Z&&kG ziii>hJrMR-$5iOv{@U*|J~+}_dT9pWyphf*L@0p364NmCaj;ijTCL~Ozh zk_5*a+RQDn2F53G;XE+i`{?4?rp(yuL>4?#4@as@GQl4-bHn=o(wxUn&;NCE+1p{& z{5&9e^QrwR1uVchF_&Hsq2x_XaY?+3>PC3(ylIOZMcuNI%cTjrtEuf~8V*|szqyu% z^h!XpGN>169Ga%fO-U{8wQ zco$Uv_ygp~EHcI>FpVElaUl){BL{^8kBWh0;#i#upelJ;$B|IsN);gKg_2OEjNy$^@o=H1@$hL zjkpLh7DJMD@XzILcZXFi+bZO+GQqR z;N~D+bI73wTWbQ1j|(oi&{+IWC@GLV3qlcyUsdz4E);=2NOwjOhI~j%4^4o12+ESU z1m+@kLuDPuiLv)dx~z!UEcSv+B?0@7LlM9^ zF%REGxo;{OZo#N%AWU)=%?m|8SK5S!U~FTDJ#sx?%Cn|$d!6FI$wd4eF`j3@IdEQh{skj1#a00a9SH!z9k>$c{>2wx zvLs+HvpZF1ia`n9(U!tI3JgU8 zUm*RUFJK!!eCEu6Bl*TSP)ytet%Qoi=}1eqK_-=|5Kkq*JGXG^Fa76~xQ&Q(ljimd z+5y$%pg(WxQon5i(*gk>k$lExqV2H=v8yR=rq7dP`lf5m_9vgS{JX9oh51Po--?n% zEJ_O4I5NM@0Zyytm@r3;eWqI&wLMk?{O20{(6i|6c*EG;>1L>PnoSFUzV!*N{`2Hh z;MJUG2m(R)m6u<#eZagqBZjmI&;~&5?hzI40fjJ2Tz!2=gx7xSI@^=Dc|sWc-~+Go zL85J(n9TXAi*g73q!0-T_QWLKZ!4F;5P&eUTN2~S zYcUDv44K$0MxfKRCYht4M@|nm#9}i@3zzo6*%=x>&|1(CXr<^i@@z!U{d!1|^vQx% znx5U=s4eW$U@sGpVd@B2(-26t0Q9aniprK3K+9gRWn!=l+#wU1C(=3~++fcZl%BlS zv!A_PG)|anc*8hf3-jb@TrMXJ`VVfsHBTH{OC%v(0SFZ;&-VXh`M}NV!SrTIwJa`S zJ|Dq}zPJ(JwtF&-C$z{Ud|IPqW?iIWddn89ZZHxJDsX~3NC0FeCIUE+fG62Z1b7Yv zw8=pOfcN1GysQKyHmI2i>2*`pUv~LrW{iSODe|Nk5uxbCXW;cn04~-F=Co5zwZnz# zf&(I!Ydp@oe~EDlI3VE@Ad_liKHCSEGLHm^sic`8kiJe-UyI3-OE#t5U)IgcbrlVg z_B?n3S4v(qEuMK_D)`GJsjs=dSCrD8ba#_gR1g=C91DMg8cNodK9GsalL`{hkkpnO zW|pjKZSNn}2xkj_Qu)|&d-*X^?*QZZ@( z=DF)(JVc~2*_q&kk#)-kJ|twJu4}c1lIu1z3m3e-XGvB$ONKobv@a6l344A*s7#hc zUp@2fL+0aN(sR7=rki??fM5Rdmpa4yo{^a(cnh4RhZQ|Wk`kmx5i4P{@jEstuXXxq zXILUZ#{WmPForl7uWQ5lq}%L0Pz&S0B)G!`VBB#Q6uhEf5ilka#*9M(gL26>!#|f? zHdOC3=107S$%e|j5AtbNpW1s$;khBDsnYaBdevN}d_uBOn~IU$7JBZh*k7+EslDv$ z){U{6Es+*_;JVBbx~2qJbNZ`h2d-|*Jers&g&WdqNQ#B+CWE^^qz5j?{03?1BkWvZ zUVay3xllMn#EIXrKS)eJR$-uhK_tfa$d9&OK4w`2S=9e<_;cZWf@5$s62W@j;rK_w z>}U4pR-J>&opa84hEhJRZh_A}^ORNh=D?}1dF^%IG6hZqyq;Fw4Uh(|!;jOsabGx| z9bupC&@;^Y9Hfehxy^D{=_t%YrOq`#GzPjiUQ-;^wVrv=@~QY>YzFZq?ir*jwd1WG3NtIONoV>{r={*z*^zvW-z zWGvbhNQC}R_>S#iYi?a;e+?zSEfF9Jg(vz?%$YN)hEygHGmx?ux|of|eE8C%2nIbc zM%b{_M1+PfH)e>n;vut`Zc@lZZYr{t2cu)Fp8=K?abPCI} zS%8lSe87BU`i!-0W+oPkr>&d}(x2-#g14=2MYIaWD9KBD0T$eVDr+Se`Xe{j1Z-*7Tc`4;dn155jbGDf1B2WT(g_vtAVwA zhIU1Am|H|vo19#eC5diZCjlM^4gRIwGgc(w9hief_QO+ofoDJvvuC~VJ+mHxyh&VH zcdYAC%P36%v_%KOj(rgKLe$USyLX=@061_`Q4)wrjAKH|9%X%>pvr(G3j}CnZ;hlI zH1sc46RlHJWD8kc5@FxZ-)HC7VTT-MNixYaL_-LJ1bE(qGw~AT^&k~Y(ZjePgh9_O zIw%O&OizD?L@OyCK>zqTOp_Q0z>pZDE=aZROU`|sd!iy8 z#~*)!pX=Y8S47)#TVe`6bPE`5hJz5%$AR?Q-VdBhM;v~H6_}7b*D|z3s$MP8L2Wj?J7{J;I*|E;^{-g|5T)3b&o7Rf6v(XD2&#CdkiEq?a1pLLHZ zj7G3U+~zRtqwNU+CGD-pWCOSw*!f}#{h=o2FUOJ&Ug#A6}jz$T_j-xFkbOUv8Trjz?bHA3>iVSo!C z@cq7!q2{yfgNO$Q4plUI<{UDJ0>Qjz^oZwk81WkDa}#KR6bZ;DoOq(Cn1Kp#y=*+5 z!|x$BaXpL>Qq4e&GGh+C5614-L*-q4FWAhdsTMkL|Aq;Y?6G74vInk_qATygd4kIx`)|$cYrj#^o>%QPK@`|G+bjqkWoHH)0wljd z#%MdRpCdy)PmB{qJ$*j={Jp@vVKkS-fqm$*y3PRC%z=6 ziXzOjea6L@Kc`5|JF_i}_thRI1F2^+b9WV$!)Z6L(C2~tLRE~?97MHuRbn!VZ!LVON`)3Uj4YKFC}T!a2}ADex&1iNc5sqNi}XnskOBn znhnM`yf}wG>zx1(d6)NFoB5!@ng^XCkg!(YR{e~xZA5UJ?M3z+?~9Tqo*Go~oEMDX z@&{<2_rfR$LLdr&RQU+wxBye1r045*f*DmUXA$QUXV~BaSL*4cR?Ieu7c@nbXngGP<_~a$CMA0} z_V((0T}5p%_TW7NaJSXTuO^vVTE~NU* zl8zhY3}e5Modpaws`c&)8P*yXX&yu{xJDL``Cx12p3NjRSPyJ?FPh^?fFKDRDn6N+ z6=PUkAx0s_>xf7QL?Dbk5ET08;}3givURWH*UxohPl^pG?B>T*QuM_aJmVh}gH{1t zfbV_pd&Y1EU3o;c1At47z^B&_i4Vdcw!<78pb=C7Ci)YyWhxl-1&&qS@Xvr<_245> zBTz>T0xBC0&?}YgjOY(72U<8BZiBvnM;)lD=>Idb%-t}?a%ekH5g7^8d=iFCUs`&~ z-RP?xkHMAOub7~7E5X{bv`ENbJNRdWCYK7ox|M@}IT9%o(-%`sipAC-(#d4&B2j>J z1C+}TMa7q4441V$PaGpt9gY|n4%ccCREo8VfLA0EMp7XB6i`Y?f9wHZ^tEm4K^4%O zT%DncoX83@E6-!t`y}S9Z%{mfIB-7u7oGt7#1}taY=;&AtpfH19w1ECfHolG3xv;z z?>TBH#01Y%>eWFZM3u~Rj@Je#@DuT85!Z&UKY`KqwIXwo@>NSIYhak&R=# zi!?Yk17VSS$2mhgBC`1>zjwXw+y{yRiWSa=v8Az$MB9w_pjA7c_jE-HJ+j=5&6G-$ zoq*(ks(`+Lx!UH;=aM%Ss=LOifY2uCTMc*-H>qjFgVMm0Hh!rWJJI6?<;Oa zE+GeH`@OW0OzjT)dk|EPrqGX!c$ATr#;l>S!e0=R*qrzC;U!p@zLr(xAz-pmHv*ZY z%PJQsN*L*@b^2~p0uhGp-!q$&mMl_`U9)&15srSg%^3G?R^zNM-?}b9+s?>8v=2Vi z!-AkG$%s*n`ev;Sh3vRLpa9?b&Ub7M8Ar0aLzPePXA{!zy5*KzjiF4>lqBFet(vg! zodp6DP}r4<68JgicT8#+bYm^Bt7oBqdd@j#o0KqB$#XigWYVF$PtX_O@Ihec5KS03 zxNrxgf?BhlpeO3^7!sF{bygY^l=xXAR7-0Cc~w}^adc~>t1UI>9k(EqOWQ%a4k39IzDBkUYgmJULv|0=Z(9 zoX=tQj+-07H>2zFv%(c@d3Z6PRP0yQYn-*0)UQJO{Jb_vbad0~q87XERpS9DR1IVe zs>Xh?YTNk&1Z@rWB>i)I2dLyfR`o#BBtVc$6Ocr0dC9iyt74nF($6-tHiIvMf^KX+O4%UJ&NuzJV!Hm;0-cQr*EN;b zYzoYIBJ|KQ?iT7}M6HuqnCq`iiD^ismnPS`c1tSjTV31NE*M6U++8VPbdDIh7C zz8vpM!)azz6yzPMiF3$nW~(r7q%EfJq_WGW68frq=mNV#*Y_md^g-rwmbIT1-efMn)8qcnMyF5%a)5_p*6P-cMHy#WlH><+RmkKqKP`Hj=0d}*+ zOjg+%W9k!{&U*R0&)H|6WqmU$Vh)2@_&luxK2{vo=hvzN`0Tsi{hskG;4;94ppOVI zfY$+fyyA+>t@^-}nt9u4KZ4rL1`LvY#eN}>uFX7|j+RwycRwHhzI%Jvxm}2xLN0X4urU9mwgl34Sc54wD*3Ij^FY_TMmxVSBh9f{!#5b z_T=PQUkQuI0=BHng1aT?k&GSCR6|G$*OV3_8P5R7l%B-5+*;-L>s+k^%5UwJp=#?|Vw4=Hp16NeY-J!cSIJ+E~fH{`Il}ql8gm&QoNr5QFru z2uiVkoJ*YKgrevf&Zar$=wmIAQNc%e+{F!FkR3fNy=lE9=6Zn*rEgozyl_Y8IgagX z^MwX<$9H>z!e2;n(k~=1N5W52_dUK}An1{Vd{<%qTW-Bo52Ib(-M_oLyI=VLD3W;_ z)P2_D|NaktU_l)pa_oTV!+UX{-DVDS^DVbn1rCu9o6^s;B@z3hkWol9sLpA1FgA#O zz#!i_>suG}^3GAmK@tZhK=@qSLsG}#%R$EhNXvi<2@{o=^Z*53dDYeW{PQe9i3F`w zL9tZhuPM>x>U${(EmkXIN$oNtFV}XLx_xCFfy9lk)+=mC4X>j2KmNP%{;Sy@cMA+s zvWH1s^h25of6}ZR+GB=NK?lc5WEB&(LfOfaO6T+7?AOEo6&cvM|Gpu`9<+0Z5%jS2 zmBa@=j>Pf9$RCyd$De$pYy6!siL}kTb86DgenUBs=c0~Dzu&Y7NX&Jwu+}fQ@B(`b zJSLKE8*iI=v7DndK_-EqU4R+Tiz?JbDzQRMrp{fFftZ>$W^?M)vVtbBmf9` z7?C8IDQ#t+eq`=ot2U`tdu!alQmq}to5n=mVXWWC0RTP+CZ`&}jyEpqsZ2bM^%3}h zAB}0OnJJ$EDc;5yY`-g_U=RO(j>3j;0XTHtQ2+8@{pv3BG3OhA*Ko3qC($Y(h|`z6 zTV;fwdHNa8fHl-&&2$4W8{*z!YyyE32Q{iD?iKxc1W)LJ@;MPaA%I)ev}0$VSu+ZS z(jc1uu(bI+hnm&t%_FXrnD6OMHaeZw$#{MjhnE{P-7#Ha$T;Gu{rnf|44SZ z(2nRw$B@I;J?YPgZw9}LaIvupMi7X;GDK!%fz5V1wB*?9=FX{PKQ4v!PttC#cA*`} z8=s}N;Ad*34_DwV-G|<(Z@88dbli|u0ncG-!1VWn0xJ8BMEp{9A!N^akAY_m)}IC4 z{Pbp9#je4h&&6XvA-2Utp%BQCkpSk*iYT7DXSbhRp#R?m_0bxHDwPECfQ*t(Oq$36 z_PSWwD6iI72o>{GXsj>a(ivA$nAs(TvSiaq1iAS<=Kxe?5-QTa6$K)xNv0+hen$FB zA1Kpe`FWL;_F2-ZrNJ~DnE9Rn79l0co>xeV2Cjf79PY5iyvJ-Uzv*Rhtq0c0NVuX4 z`T@SLcet_M4L#65K0_QU(Dl7AQx)6Zi-xdqQ2Qcq0pWibw8Yn97M^HmrA`l3+wW)D_oeMpPo$lW3fA#_6W>fR!otW2Yi88wLUH zwaVr~oq47uLJ|S8;@Q+)ubO!F+20eJc15H>LcmMZv$09)!wrVeoeuUqDKj zzbmFX((fr8DXb;d$~^FHdXJ5CzlapcK<8z@L-L<0Z$>L>s(-jMB)^{FThK*`*U=ff45CV1S2kRWJ=9u+lr+X{v_X zjObKdn4R<}1BnQS&YgGM+1+&GO&0X;zyCg!_`T157bR1#z4kg~{NHT%7QVp4nh3x> zn1$eb_F3n2Kl;&+jUkJVz?14y2J@fK44aui4iDfGtxkY8AAR((p8Uw+N4tRolq7=$ ziNleDYnLJhetvh;f;|b-k@`69Ijv7#!8 zP(})ms~l2GqYb6S^t|!GphoBs7n$P8O}-> z^bSZcSy$|J6Le|tZc;KJ0T2BQ3y_g$>~gLE5I>c-sa1aZxt&2KFckXE+x5A}X$60{ z77plO5IoTZf#erI7UON8Zjk!m53y6&K>p%XAs%|{4rrIpnlyW1b~nc2Q84Mlvl+2KYtf*(pZ9q_Tf$E$#7p*O1Bp;ZK@WU03kzyAgAL zte>f`GatPH-2T}XMMT^x1fo&JLXcX5E!k!ByhHCKN;dYvxM8Fc4<>@cdSH#4pod76 z$QKU?9>)eUWhdRZ{7@CxSWwLJ3kW!}0ocsEAH6mpd78Cnv6r!?ro0FE-l{cA{@fWvFV-ufMP++kHZ3V?Ur zb(f7BQ62C)96J1td1R1^U;N@1-AgLn%X4WvFp7gZ;KdhTWCs^~0j2`{y;`$_2WXQ7 zfNFtRZ1fj)sj$!XFwaaGjs_hy~&SuZUr;imT*-e^09*a*AS- z;>m)i+v}oj!hYj<#(3AqaUIb`6OVwGRE={pBr}vtw9QV{F*gW>OdQY#z^}u|g2}nC zW@EOSw#R7+qKpSOX|UyW&32BLE8a9&INBos;cRuCTMW}LG?Mzuh)MnxG&!VT z&3A5c)JRJ4?Bz`~N~`1YM4PSUWL$jnN&% zX67M~o2p#paL8sWogc{~NW@Vc7VyluI(iU@J7mjjmP0}UK<^Hsnz#L&1qwl`vfyuX zY#*2gI2*>uFuSeNjb;{IjqKIC+xcXL=VR^3yzcXw0KB85wtmLAn-1O)He}ON5rZUf zT^|LRVUh|Gfkn;VD7M3e_L!lMxw{Lp{=t+on2(@NbtjQM`(nX0~p9|89b?EsPuMh_tw?hA9z zJpXj(2T?;1--X6{64b zKHe1=zv1%N+CNgv-#06z5)BxQPD!r=;A+BM*+d(M8)#+ z9t#9PFJ|Ql%p|$A^;q+8T|QMbY&|mh=UzkObPMpOT4j$W0H{N{ zKuHb|u7RXXn*ial-M!|)KosWeF%h$%j_1`r!>F^%19ut(;0!Q@!^9Aujl{^0_Evl& z7C`c{B*&(>oc>`EON?(}gMfu0hUDwEdh+Ovt5E44sr^Dv z$n#@_rZlD%F>rW)FiqI9MLz?A{dwn~r)>UPtonw@kMu4^9T^f(d5s9+QvrZRkObfw z$Y=Zg?|*M@e@LOiH#l0e{99EUfJ&d&eMs8?jP-pUpm^>Z_NBM$WB~`+L)wx+E|_bI z<9i?`@ClF|#kYY9KXMXCqWIp=Q#CP^(~t$e9&ME^3jFL3AxTQNwu-Nb>{kgT)!@Hc-QU#>d!}ZurLBbS zD(;+3i)EK6f6lyq278QkHq7#O88f~Lc7(Gcdcxb8GndR=&eY;G2=3IeU<85Uv_Nc6 z#l1=b0!qLH4=p^n0Q|j;FvCyKBp4KsB%C0Q@X-6(0$`8D1R~0Z!v$cBkoF;H8LoaV z1ZVdC81z?i z*%TS?9+4H-rZi15mviFnTDG7E6e&n0gnP1HUa)2yyxO_cRd{;RV#^k;U6Ob-WFSCV z-J}&x%JWMc{}vriuJr2;|6Us(>>F{i&~L^Sf-UsA-V)wlh*v=Mx-yc-$w(_5S8>erT^x;HIy~XUeL&JT1@qy56FA zpHur`KVDtNvpZrg6D{Lc_+A7ckhmk*2%a}oa zK*#==SB||>4h+-*BX@!Xfy2vtFL;s?eNr_2SqBiAAp0?b#ogp_$0%w7(TU3G;fEi# z1JXWgVyRsA6T4!JEI*Pak+G!DFICsNx^sLpEjluioFugAPHxVk>p&L z?W-`Z<%4txhH2|E7AqvM$7uqd}Knai?XWY)DclA9G z4m$U|bM19VYM#|ZIF7|e_snw?NyAML^p3GZ_8Kz9{EiqkCb5e9DtL&c7pliHvXTv7e*F9p?M#y8|B*Y`qcAm;^Ow zdi&q=Jnk9eaRf~g0)OxJW;Vf6zCxidU`%2C+=z*M=l0u`TX2!J0D?e$za@VD@BePo z0aP|zDw^*y#Hq4q#gN>5Pf3Ls`!MqNm=gY?(F*SjEP1}N{ox(JeL#f} z1cDZV-B&$4k@=*`XS9em1fnRQ0GCYAeJXPhkMX%8{dh{a!G^r;E)1E{%B4Jxr zIGr^ImGYyqd%8H@7XxpXNCj8-N;3OT>up}?i~F{~%+8`6JGa~8Hi90$CI1^<$9^ya zawg-=_;Ez8HUlr=NaYh8ciiz7tRc(3hcAjI471r<-%{|s{?lyFzpeWNe*hP_G+dRn z67=^J7m)Ry)3b#(r)iMrMMD^dXiF;r;vgT-D*?KZopsa;ShvyRofiC`dFE+Lyh$`D zqT+oWq}a!4qO0bSZIXCPx<(d^)JnPU7OdrE+5S^f+NBggQKY-Bt%}`;r9NRC`EjJB zeiolQiEez>ejjA;eH(cg1C)vEMWh_(negUX0UB{lhYuMm1kM97k3ha2q$xS%qtxSg zkqn0&bLp5#@Vsm!tYxqmDtQ7j0SOq34gBe+p9PlxtQkP!qBQdx)9F6hzAZ-I$i`vd z`*E9ury2H?nSbc@vbiA*y{n=)rk1B#-ULrP!KsW1cG1s!vK0Wl$*7&eiF>};)19x? zHM9LMzxSVV z%{Los`O!xn?f&?{1BSeO{gqcW#3SHFof_aCAxHut5CNfR+Cg#v7k~r#4-fpo5Qg|; zN`ZP`c?~8MFdBjk<-HqxA9QA9sKd;}g!e_kFcLfe0#o*5QKe;Nc;x_F0I%A=Smy9L z&~w5nDsV~+kk#KJ0=&er^?EKRA zVNX~)QUm)Qo8x!AOQ>ja3sT7*V!u-`SW>cgUp5BGIfhvm!JDAWIRQdx?aD6bc<2vY z0L~J)HXmwLZWVm+;vgVXXq>H5+`KUQ9wZXxc>)5U=mR7~-1JBkOe!3HgmaDV)<7IK zLo>Z5&K6WW%^+=KV<{NlEwTMdVVy0t*On%wZr##1Pw1!2?<)GTZpsl*WJ#`pjMaT4 zNdi0q;8Ne=96`vZ7OEnI$p)WiZa*13nvn&i)D;+uevcqM8@CDem;?&X0od1-LQjLX z{g4-t0I;#*^9Z64W1_fFjW{(ATHXX3`f_YS;AXSnOj`ibjfi~F3X=eIy3V!}d|ptM zypKQrB(QD0P=;g+-%Dlkn!tO<=muxrp8o%Z7hPy5gl$Sy;$uvxpIxquK1t#;;9bY| zSmqD>&P+Z*_cy=!jlDLIqK6+-w-@P33y0_=$a^2JhYDsux8MFD-*uKEE4V0Z^MHDEdf+w9=Na0u4_8?*}k}{IeQaAXD*|Bwoq-p^<8bvNapLfMPTLS1?AwDD#0b8xHqoR>;9IIP zZ;jLLWn> z8Hy1kVo4YYnoOUtUX9qQ=LIb(Yfo*?rz1sr?nyowm-2h;b+w=Xkph|e>dUW!`sS>^ zUiOE{3u`cE9h)5ZH-6UN5`|6QMmSNE-wTw;$1h;})3c8*g9LikI?i;A-|-rFXwc#? ziQAw%0+NQyAp4om{en*JyrHO>Dfm$&VR5VHEqWjMeUPke=B2pT3|WMn%WJ`5g8k24 zja;Les%6@atF4L}3Rm3^)jcXzqpg&j9|Lr4&Hm*)z>B1}xOqzx37Zn2J=-?n0GRQOjdxXi z+k-AYkM%+PNP8~95Zg?G-z>@8Ys_!&=5>%QW;PXsGP39OwQb2LV0|I*c;1(}U6KYw zH%L69I-ksCWc(6a06=`84skQHiH^~r$l*a7!dJ8#p`w+7$1>KTj`GJRXkr1h4ovr+ zCKqrDKKt~uprb#`0N3WM4Mq<_DP3SSL)iPycW$@)j90(|TCD^B(V_q)xb55DHUl0i zct!)C(Lfxv$xk`;jRN}hz?5ri-r)=;|IJ$Eqh9v=-~T~h`vE)D4i2M8 ze3$e>K>?^Z9((L@I|T6=;5&kkKmq`a`kdZ-Lvu`xtWE}8)I^s9G8VLTGSO7UGCj=U zI$8C)n^n84JZU9ce*Eh1HTA%njcil|Wjzw~K(Hby6{kEDL`$pjCafF|*FkzVZ@*Xz zVCH&fN_a7*CY70bON1h%CQKrCTldFZUUiL-6XnG+DHNUF*L`NlD=4%^JGK z(E+dO3YRDs>cMlg&dn11cs!1Lkb^GJ-Z%t4s%cchd@?l1|4LWMtOGlj zHvsh@qyP4|zhk|<$2Dleq-S&}T4rWc3YM;9mV5C;{Ri2>V=h)s;pVJWTKLqe>>cU#A9k?fSBdcD^^N zI1udwl8T4bM*svSt_8dRPSynA=9_P^&-t(l41M^)M}Z(IrYAu4!*x;Fao9?;pIH1U ztHuTDUdxe#mKz^xYGF)Gos6og)Vsd+x`h*FO^dx*tx1JM*^M1JyRmqW)V$D>>ye5* z@QCEvU>;UYM~lS8x!M685&%6*JfZX(?;&AImFuHXOsSwKO#{h(Yl{~b3kU%F7C7BQ z>iPAUP>$|b)-R9Uwd->WtkIp+#uat_b1ng|)28?$6b0-Zf3N&|L5O8yfZ~VtBg9-# z2eTs%+yl^t6Hh$J=KMeW;6pID=EBZC+DvocGf)Y+PJ%cWR|6K*ji|Bn2@Ebt;;^t-vMn5JTKY9&aEg+2Y zS1#+V&26j&Aq;{LFq7HO&SqI>q^9yjK&5h|lBTalK=~N0ZT(OnV+G^_rh!)aFJEgerpIN3b&|yU#zjz%>u#pO!@HJoEx#@*)pRkVfF+;!7^> zUVimugWTZ@@E9`rxk$qxNHT&FpZy@+@Lu2lG}HGti(bTdtV-FO9*GHimqTh_7#X5p z|6l&g-X}>q&Yr0rgYq@cUn>01Caj^Mqzm@us_hC{GlRamVAcy*RJX7kOXFguFk<0m(Q11HF2xbCc^#y7oMqjIyg1nRlE#Oa=IAJFYUF1r#89O?N@t+7-~& zTmu~=guw4JJtUZ)N1ZPA;9ch$s$=4%KJ`7>padu~YDk3i^=-6g+j<4U@^E0(wx@!o z1wjJ9^#E&Ap}hRc%U;kYn)|rgEbDk-TKht;iOK*qF)9P#d6@LfCX?*=e%{ zbw4`A{kv9!@ZTsBfN2nIh5zwC{@?E2`|k0628w`yIKUSmIRETt|7OHNFZC5-lfVxm8KZ)r2|nO@Svi#9_FIieXr zvcaLoee=dp+ecE!HF{7}b*@}*D-e&QljTiIy8R3OlVeFMPY%LHGan$=XD?&S|OqP z1BjlzPL;kHk_=CT784d?{1&8?AJA0_56->qwpTW*{dLn=(L@A9@F|@Hdi%|{Ooi}F zZ8Jt~1Ls`5lFz1Q+(bWp$yQnwsgkWLVM<%LOP1O+%bRAE-Kq*$)#f5}ne9e#Y5AGr z=JP=;NR|2LsSZG1bR_!3xp!edflyvy;43&uAOh#J0yiQD^;pN~B(1=T{$F~9J#&`w zn$)n$zcO$5VBC=mc}gNnK()+s89`%1zySafiv*l`p#dK^J+5GfD3K3R9?&`PzxSH| zx<4pNa8Q!0lTAvXVlI~;M2``1n>|6$qpX19EsY5PMm$_E(A~Rg{6R8-zz4A1vnt_9 zur&hCnm<&UVpri;o^8Boa5xO%>vW!<5uyL7RXfHapetgD!e&t|rP#RZ=Xs;|0Efic zTBTzegiiS@D&zY*oeX&5iN{sX{Bv~#ysZ0&|KtDcuD<#jlQjO>&wi?b{%%WV2<$+R zcnO@STK;$m{6P^8q)OybSHx6B=Z4IoDY1L^H~qe5+J&Tj zX2_W2>~uGzb9_Ftzt*B)WA1r5QMz}qfAvL(Kfew(B$M~rvuAH%Slk84@Xp?w)*Pr6 zg-0qJB(AO7C#NS}E8iSEDu z_y5Pn|3F3HH{7Ax0JIBuUj&9Q4MIyCDP!6wBoBxX(OSTknAv515K3ay@*+(T_+eV$ zjo04@R z_UEnSfDr@PgN(f)sf$of6yjpNQvE|_?Sg=h6k2G*p7NFMm%;jWEj8@w4_%KE5NuHc zq(bLRic?H!G38qcJ^^7L(lX?a3w))hJjg!gvFL?>0Kw|y1KAy7#ZV{XwFL<<3 zRcc-4dvhz=R(Jam{8!%ltBmCqZ88~G`zrwm6PK0MONEZuFN0!zg5aLQrjmEGWs{%iArF)fnWmr3FI4edcrIBn zL7ZR-d=8^O2F0=Ixa<3TyeDuRRcd@Ln@B=JGEc|(()oGau%<1pe=xea?DEU4;sxFY zK^PE#F^ivI#o+|YozDu&K^x$69UZ##vP*kr{j28mwc%}sQys>#M_puu=JsczI|nNh z|JzEoyUCCVNO%NP#`tn4J1HblrRO`u=>@zy6H{dPtWz z3jW=H{yX~)Noaok>tCCQ5bcBuf?Vm0cj<`UVljXSiPiYGLRBq_~?W#XQuRZvBu zJtg`{OOr%O_kdFRElxmpwr1&XzocrEuf6)(V1%PZtuTBhg8HuJ8KajQ+b-cFo=O_I)dA{rTz66tzjq0SyhlDu03Fe9%N53wrT zv{u3IMB5A4hYVUXZe0+dgSk+TlI42a_wCVjq{*dqIDoQLz2>%rH9 z$pH+(Zqx#R)`?9w#Aq@?&MM2MHZtnEY?NEdo_0gbYD%H6^=E%oIIG-%R?!>_D@l-m z*as@d?yI5*$rFm3<2ii2;8%aeKK!~6oWQK{Q@B;R9alSJa+8n5++0S4 z2iNin@A}+1km_{7nhd4}Vqh;8I6<5Ry%E&;>MG)1go8~9X@&*LX7#Fm`;x=3qCy&8 z&aR^-j_f*`%yc6axULu600yHse#xbm82g<*H-8VZFgXVvUW4rYw9`&C zPk>i7NRzpUW+_qCFb0%h)zTyB$WFYmm{fp=;4Q~!rm7ImP(C; z(g`kg^@{zzv(7wQ`~A0@y)XA1!s5@B_(y_4m2v$IH!2eF4?3Q7imCuUYWe}l0W))a ziAn_DdFP#mAizub^{;-df<=rK?deX{%7Vm)NeB+ldsHFt5k(64+yrxyfLnAd5pDzW z#JHTnv)HTO@liSfa+6Lf@Ypy_z^JA9N+kZ|?Rnywt*4CaNlV6lz30FDIF!r$?Q7y8 z(oofM4N{D>$xE(frzhF#jK@qD%Xqg`c_eLKN{v8R;Vg{WDJ^U42}$7ad;i1!lNVI(^C5L_FIh7E(% zXPIpH;TqgI*9g?8gfa@t`I6pi_5m~bpb8KjQO*RN+!k~(ECJB>gY?JV+o$gb8c1=q zkrY>Plo0|V*F+KGMY=?FH_la2p>yrCnL_KJ2GhiGTDhY%jn0-w1GeJaC$bm~7$x3G z^IckOyhg5DTIba~0lKl)g$t7dg@t{+1(851ea?{nCJ_Q^cwOYjjH~eMa`|A9Lg`s1 z$xI?+g`yFeC`iDvkg15tQuzT=H!_*^_}7YQg5h=S_quT)5Jpb`vRfRhY$Me~ze5k- zTn9B%KTiAjY%~Jp+i$Qfc+!a{TPxtF|MqWo$WW7iE&1N|CmJ*K82S7W3M2IBdf(tG(uXA`8FrHt0 z@g*}WI$9F|I0}cV=D^3n5s_>4RqbqP*s$!)jSOc*y5%}%%c_^jitwk@54EmB7oO7l zXf)?~_WT$y8(>aZCio|nUyF<=6%*h}LNNJO*-0?2X&}Rau2Dz;dB^X8GlKmTl_RH! zl#w9EUfH(3p6LtJzGG`UhNNOXuOKLqFsMP7K)W-HJ=3y`q+4168F zlYsMH7pvtsngi>}{Th7-QXy)8fpdKxf z()L#>MKKlqT%>%bdfFk4?!NdOVwNNKy}o~zph(3~B- z-FweHN(%ggj{m)3%w!Y<5ePc#?6bOe6|J~O6AQcssFpYf7-#zKcfV&n*?;|)f9c-P zgbC6lixMse2muHVJ_RILQCO;+>6QftRI6flLCz&J9u}#m)Dz|=DcR1sE+JDbR^5}9 z(MjXLY|S<|KfV-@dd=#0bO09VeZzmDf)Ol*2DE=S%uGdIiwiw^$aNol@SdFo@dpJ! z5JuL;d~>re1V-Gb&~}6T=LN$KHenqj$Hlf_MP6+d?)^qkwand#d@0BNA(;RrJSYfX z!2>XIYZx<}hs5J>O#mPnlRSfLnm)?z-95i4ii;8anaCmNlPI9o;gJFfd?pniI@HUa zlcZCq;3NnS0EN!r_+zgZTavWXKv6Eg;!1lSdpS>r6T9t&+82oqOLL61ooO{R^s2IKAr3t`m%ZfLKmOxC{__Mh z=@Ra@TTsf3)10yRzpLzMw@izbij_^t2Y69?!+60ne!`+YUoB!0`iVwA18K$ejBUTE zwiyYJDi;*t$}6uhD3}Bw8UVSx7t?3aBoJ>aR0ye!jgI)ukxIZ}Gt8nIVb<~*HZ_?s z6AQ@IviYc3sWO<8XVhm;Fv^I*zlQ@gpl=U)lY}4}%E9=y3KnsVygmjzv^~tKG&(3&t5UxJqwViTFdzc0 z0iuNHM^BznrS+h421>xh0F}+FufAfHIoHGEk@1DE0h|dzi%Nv|r_YZw*&%`WNV@_q z0q=Y2X{T64MS{ghA!I=MlN^wclc@F%4KmUkUJ#*321rJB2ceUAzj8r~n)UX9k^if+ zmfe0*;g+!DT?rm_OY}!q{QOIis*|||>6Wr?mC4@?!NHH>&22xvprSV<^igdO!YB@Q zB;Bkf*7LkP7ouHo=M_Jxh7s#x6qU1x{j?`k;c;I_RFBa-H1-MT$7tX_(TsmO^CFmq z(N|L<)b))UAfi01Z^rAW;z^4Ah~0+0Ca9J<{yp=IeFj=<*6%;$;7o#-eFvA|6&;0T ze*)JVmntNLp7HN2fuP7BQRlp66q(17e6Xh(fhKu=TL19zE>*!FCX+}Yc8A2nr75wC zppu)|KeELpS$iL3!)q-RXH}k0%UyDbwJ%d`v!Vax>euV4s-AgF9mwt#u$9k~m_*0H&W&Xl*ysd(tYwj`g`{(Z z24MoO1c~C*S+ATAkG*C#Ie~|OcbJ;hKgR3B2jwvl~+x-oJ0fh5s(ZH7rr;_NcntBJrMl4KO{)>JsIKSceGMSoVf2GG9(RD zA$)e8cf{eqp~o6ZM0L-B_h~?gj@3~oWRN+GpL*&^Yvr7N`Wg2Ak-yNAeN8+y4#;TH z@-zLb8J*OX(dtdrg=Rv|Wdb$Lq(T<=|7BucyP39BVbk=9uBtuI9YY$Y(*zA?UfPe=|#~OR*Z9iX6GKskkg+@#O0@7-gJIEvJlE<a4oFRsEL}~Jn9BqUo?;xq`TvD6|uV25$=iaz# z)(33qVlKqpR~dT}0ESwg)n~nvB>)K(akARatEIjt(qcZi%AIj7J!jVvNC|y5{*w&^ zwEG&Wa2lAeQ#cth3!ebY2UdK9@i#rIo@9X@`U7EBoW%_+Pi7sxx+Am~36-|_^?y9~ z6E*XBRPAedE>-IBx`7G6R{7ROUR*c0_EDwl=H#%!9EYAYy=($nlpjs8gD6k}BcF{9 zc?YNxE(GlaHuxE5o?!>YD;iko{UObJlzkQn40joHmm3w7>4NM3$lj?MP(9?3!W85+ zZBy(Ey87HB4y~B*WDTaiSd$*&j26HB!DJkl0$0r)z@CN-*HGnn2h65%>2{Rpyv;HkVBmo1&1@A z7s!;#2z>~ybMM~0l|Y|6k1b{a*LfXVKZx9F&V-TaTgW-5Nq{uyg5xqp79tNafV^Yn z!~{N+Vzg_Cqi*DB&`J!lUTEd%nNir>dxNk(AA9Uk>tlbVNK<@W)(C|Gi8k=Ik@6%Z zWQL?Z@LpI$AL#m`oya-{Ny957w!#Jkz!Uer3JJj1Aptl^HAXt)8ii?|u)l!xNpN4+ z=>*8in1kVu!8z2EZf73V{8GnyX`g}Iv5w6nLj$~#efRo(Xfo)!?%&jX{e>nB%>48I zoEHReLpnHbKw0$%#Z6w1`$4j9o&!2PK;{2uEx;%YzNP0ENkosJ#Zm07Iz_fd))IBr zRr>m?*#&P=H^51eV@o}JN~*-+8d1_Mx?BDe^-zBgz9x<~E~*8_wuz*S%rz2#jeT={ zz;pU-4GF+7=9AcFRFD~|_bQ}H2#i^ZjCvlUtVmjV1j)u@5%N4BueWqIp7EV^UGV71 zbMQxEUC9(N8R2byZWYN)vVLBJ08X~gh9UDM*!Ac^yoAt`i~0c&@%Se_HFW;jM6R!R zXJbV=NBTW4GG;(uai9 z7PSqipdk%%aJYK5IqF^e!^a;ikRAEV59?F_%7w7+`JC3Ohyx^&%O;mh@+h&)cf!5n zT4__@AwXgQYOzOu2c%CW4%)zdrM-hpGZMZWc5omF&>yJ`Gt7SO1*ig}gH$oJfRI3? zwF7fHx<&H$jCt{1K&~dN64n6FKNZcBPdsUDB-Q|bzdP8o=IUkLYMT9jfqCN95rx`p zfJ}q@vQ+~fsiY}w@gBF5MQa-Yah2fSzWlr|%w9ti;z4cakyxhR159{aD&YzxJnSKn z5BJGXy}2jnMVC+#09f>(i?;V>Ndgdx1QGxOJ4v=704JW@Pvyj4U*Rt9kT=j8PE9#`e7g-TGtL>LW z45=$x6U+7#WYDM?jmx+peQ^2oKnY$tQFK%jfDK>*Z2`Cx0T}YEVpcy-S&{^yL4Aycyoyt@H8B0biz0 zTszG7;2TRQ+@emhq(8JmW0pahtr>He@paSiaDDb_& z2{B#rV~;W91k{Hl=@KP6a*%Rp!xP{$oO0?Zek5w1v~!w9I;$QoTk{tmw+CFadPsM< z>sEB#YdIe-deR6WkSsB5zKBeOeW|yVCL8;%;6J;D)bZRG5UeS_drx+K!UA~ugOVLa zHirf=o3SOa{~zc8!IlL1Lv8EP62P1XLLTG6MzBj%GkXy(Z5fytl?H8}0GyP007Qh1 z1b_;h1OP4!=po5PL@PIfWUx;jHtaF)DGI>;IaSv)=otq!dyQnA7AOS{NClHKpXr{a zh(WeD+N4LMfNS84{6I$mNem7@>PnuO* zV*BG^xoUf0RLKtOQ{0eV*bC27KsHY*Qg;5xF>7cANFTW)FFX1L#040sn7Rji0wA;J z^f5jGkn%D`mf~a75`YDy!P;GO?G(smy*(Q@3v|X0HH&^+d&59*m+OWF7LG)gl1bY4 z>Gj{q)CuG->*3Eoue|a~gZv1bRD)}?*=WN@ziUQ?$leJ;A1EA#5f0Di_XHP9P+C=X z$c`b=uxU(=*m`j%8I&KZ<<#6L9$dcPf0P{_4?Xmt9SpP}sCGHj7*PbCM}n8$98f+L zG}S^U(kmslgMTpx@4Rg(l`@AQq#vY4PdM=egHNLyVs!bccD7_tO5@&AT^kah6$b~; zp51%RA(3FuwNr^;{KG$~Q0RSf2-)Y;o+hk+`kNebOhYin=`Bl@c%{DR+cn`qk?ci% z9v_N|o?a;Ti3*C(KmvhFA<4sG=5gNH-;r#PL_kc8R?g-3&QaFZHf zRp#@m#zb3`+?D;2g$lSAUk}?rI*{j+Z(t+<8>!1+2%HB9!s`|{2 zwg(2?J1JU;CGggR12rf1`loi&T!EBN-e(4u0FW^1-#x2(K<}*mU;p*L{&SZr^tmy@ zAf=>mj?d@Br9|_oJ$tBUOJvu|b%R25?t6!SdK^X!B}+uiE>KJSOHwZBC7x9R8KliGh36$KGw6oeM4 z7zLz25Kv~Sa=7uvFZPae_C9Byb8iCalU9*+^WHP<9T6*5tmQ$ww#iBk)uij>vdfSv zVFcs!MWCiVfJum}9P-&Ub%qh6u1l)*Mw6*X%75m|LuGA?HHqx2to$pto)GJo? zQF&ot1R8dn=VFPYdnm(}IZfH}!w z{L&htsi}lb#qbfowW^TU;NT}c9Hau9hIUL9^bn*7_&?{~@Y>^SGUysb0WORJJZow0 z-gVM4As`Y=o)h*0l5PqR0xtpfIdfuR{r_>ngE3H zwqOoO%S4k=dZ>XUQkb7tUudL*{zK!#x{ErHu0stBMwZGVhSg61APF${s_okh>I(Vg zOIM!XB}ni&^nZck=|9u=VF*r;I-U0wHO1hyV%U&E51)&3TK&hs#HYOlDVqpG1Hp03#6&2;vZd*s6u~jn40Tv_K6lA;}Ur-ONr%`s92L zu;O#iK5ur*ysr@~wKAge0I6Xfnd{Ge26Z9P<2*qS-c4TV+zU40#->DGv-jll7^gyt z8T5kI0r#913gd=cFAkcd3O@tair^lv4Ms|r8)sX7Q_AUc%LacL0YjHgJ+@4ysu2;x zU$uZWwt%ovMWPOqVysIOzJm;1GXd+6YZfj#Bs{Az)NzA;Sgqr|N z%YrX@@jT~DL`BlzEU~=fhjDH%DOjzXlB~Fv|J@ZLY2G$XhmRCSS~8&1tpXnH29fH z5onEmy0e3uY=++7gN4oSQZ=HJ4FbSx=qK|tua-MlTn1ca7A|8vRO`Tx5aC$p@Bl~( zb|ujde}E;Mp(M~!&C+LtNQfB_aN{*vu@K}2jA_kw4D$_g?e0A6To}&EI=w{`ru@J9~U(V1JRsu)xG5r!_Q{(yRe6!RbCPXeVz#*RVIUfC|E+{ zN_-0y0LRX2!TZWG8UU5H78NiKcO>jnRrIfB4A)nrl8+hcy0 zkszRKrV(rd$;J`$V~2PIlO9MA6oN%cWy?Z`#KvnFts|pH7zCJ)4Te!2iVHTgma$vy zRTa(aKiCPeeAEs z+Na{DIzeKTfX=8N8y2KJ$d>0SAAm}2x86%6fl21rRH^E*4dz}@{S)M$SC%&$FD;_; zb#rH<1VMRCp;g=@PuC5V&GOxJO<_tCEJ!l@qk&u6gbLyET4fNbGyg-L-Lyu43?tqa zHmzFFm2&!~kqWm%Dm*_~3bE))faQ{eW|HMEJ-9iPBtS{xUQMv18}jbG_uQ>j^2@D0 zhu=RqGZ*WqC>8P1M;}x8TMXot(FM_#9Ey8PrVyxUAAxGoVi@cnh>&fI7Qn#o_=%91 zlCVPV?c~ZsSJ9`*0QLxzZ%3&GcYV}K!Y0RAOoYmbill~jKjI+pZO@fsmHBUcCcw5nkevs1)wAa4PCkbB!f2BrkK>tZ)H3FeOeVL;@RVz)8xYKTTT+Ua=EQ z32tvG0zlP6azLOf(2)=(ROvctU&f6{N{-P@(M5Ck%r}(%Orip0ZmMJc4E9{hYFm5k zGe)Un{tm$r0v;O{#7H(B6bad|2;66A<-_YBFOeKTMmk!P9Ih7?$vbbywg>0x?v1nq z*kq}QUVPz28-vcCM31VLg5&)wb11BaSe2nT-eGPh}vXp>k zv(%_usFl0RxvDHB;xi<7iY&+Tlg~gdlrf^;(n^P+P#HHrizaG^!2fvsvBz7+yDI|n zMPR$)$}5aVvporp&FAw`H96K4w%X4>|E#TNP$??uBRvr={|(-gwT%V_ffWT(o;jQA zmoC|G1a8(Q0Pov#ZwbITF#*`nYTg5|O*bf7Ky^<`j0Dd=+acxermPOMMix-OF^vQ4XIq>e}Grvw2uFG)+ z&>=TBEY4?oUM!(7XbY2{S7phEOuIj8pi2fO_zIb2bIr5>5O~sGiAsR9075oFB|Mh0y{QI7NlVvC=LtO)AAcbJw1?f$lHb;T9{0vJ5>1agAYb=O^I6)DMqdvZ74 zb4mTBNQ{|V4~rz=z=5ZY(Tf2MbNxVr-PAx&v)vfCF^S;686^VBhe=DIY8U?0^OP1l z6(+9*!sa+G&{=yB0C84wuHVqHF|GB1s~$lPA(Mw5e8^yHAafQl0gH;jp~8nBe#ini zZiFa#Iq|7T<78Yv&2C~6J>gFf1vXW#J1qt(o8ywnlB*r|lb70J40(%i7Hv{#2LLs4 zZ-M-g`DLRyUq_lQyDXwRBz^Q;04p1~pB|`<59;PfLV;o+ zND&hQvBxVWuJpR9xDgOQ2WTU_pveLxOOl17lEIR*#6t*+mG`znFe;Z*8PY1#6_c;u zGV{Me6Oxj?83vDnG!wZ0c#P%|#!UdY=Xp*qbYn!v)8k8Qk}JwL2zkjYXNR9du)1OU?OC&s4V8=o=8?$WV# zpAlL6Jp1pfc@JnK!TM}<+Jpzd2EMVr(IR?O`PN3$MB&9W+Iq^VNfMa0V&YD5z9c;$ z5F7^y3?s-S6+HWv03^*3_ydgl9#V;83XRwR-w$N z9n1HBME`Ei4JiGIC!VzTg3V6=T062<=<)aAQz~MT44~x4H2@P_PF2X6 zYLv>G1-6-dzWV{gNlAPj)uqkbY5+nZkLnz99Sae-(zpou#Y#7yUOou`<7`;wKBb~Y z{5OO)pnpDtjbon%MG_SYa=I}v&cks7`SC{1a9%W1lEaZ4H%I{T#B36bLQ)}6ku#@E z1y75F0B-GrDW|=Zac-vSgb$<50LOt!iYlCH9Q$Mx%ud!;N1X59XUGQU&d3wNpPA}wl^RP=!( zwMmM?7kznc@w)Qf@W0A2iV`Oa%V_*>SwX$jYp9C{bxz^s9=Uw*Z?;529O)~ECD`Mk z4ua z%p`>-Fdme^+;0btGYJD`IEbx5)NjQ3?jOtCqYlS>Wp_N*$LZ z#LqMe`?9fEJq_39t@{bR6!S)zBI{R0zD%#}E6;xIM@s6kA;{g)inwTQSg{PYWq)K(zxX<>?Hxri?o~kb!Wbr> z#YT4Uz(E^5WYa(y3?(sl85>YQDnTrtfr&;!8&(;yAoZh)!S!+L4P$E zlj?|M=n~}%0NW#6gt{9^&yJ)B2#%@gFT;1azK?tqQUFgBC?!weP1@^qhHCi(+Y1jSZ#YVy>>^pK!T zCj`#5;l81K_Xl0$aAH_5eLjc(33uc=Z36QkU+6-fssMXEfHHS8E}iY>E43 zdiJ|>i2qLuii(^40xkgTf7troZYYYX^w}mHr0bi&j$AQ;nhJbws-^2KGPi10#M|r{ zhWyHVoqFo&Ms77^4l%Jb+H0ebx`zL%oEg>#0Ur*`8K#c8(Ix<<-+^s9qlk(YFcH8y zy5NF~?406+M3@Y;GCIQ_KBK_ar&Q3aCJW2GSIgdCgeb_O2335ty8WY@hAU#B6s`Dz zloNu4e(=xK0GrM(pqmj+{~=}u1P4HM0K~=sH35*Gy>CwH3*J=g)?NSZ1!;y7aFS2a zm5irM2O*R(zmoDxpNN!T_}Du>)+*a*2=i`~;?y8apm1lpKABFE=?}>R4>219SuPB5 z77PoMYA~8}_+<-S@&Ored$bB=6F`BF;B=I-f(b(O(g?nNZe#yzGgTf`qb%a}3=12%tOXXj5pBUT4w z^}G)=%411j=uPx&rj3Nnj=)QjfyX#)4Epy}L2xO2jEQ$BGzct(?Xg~4yU#cQ!FzKZ zNvJ?I7=@x=j~NrMVY5QPk%M|l$00!q_&s#h(fkJ`Ko8*esucj7&!%~iUK^@=P>X|_ z1fVm(*eEiKN!A1=nOd|Kkmm+GwGew`&uIom<#qn9H9I5D@H%&demfDm|BMWr{ z(y(p4?IRV4wwZ3j^>?*CC#sI#<*9I(VENZ@lIf$%#{(_cX%f#G#?%VYA4s2h(&o>c zNoVWaJcLuKTh@N8Zu_!qog6v;K+^lVCf4-INix}sKnK_tq(?^NuONH`(`S@42vLy4 z-25zhqFTAG18evLL!(}`wd4svL;yBg1cPJZd})u7EOXw#|0J(G+q`x&?U}AwVs^i; zMl|B=bIy+B!h{sGZ4Pt;6wTy)K|4;?J%eK?iYp&yT?D-siD=l;1VAx1D_b|nU6jf{Vjqp0f4bL9&#$X z3onW)SW%p3Wl;Td98}pX6n-!2Z6CH?Izf~GS00a%F$fVlyp1o*u!0q6{TdQ@W>^0qP; zq84kArZ)VRomA*|Hl-+P!ZI0tYtWPQt=aUc3}NKc6Q(k}?)vNOLB@CkWx!&LC_(Zu zKt_f;0fFGc{P&IOEV$#Sou+){DquBTaV{tU7Zd>^A5#HTpKJ(hBCV*<2!!d?2gwF- zKOO;$vhhA71q4>*%i9RstTu}-Vhhpq`aedm|6OZ`SU(a5ylzzV=3UH(ZvwK(Akz!i zAbYm<^cbbHa~+UO!uaCKbqe$xNCXJdOkX@_Gxjf;COQ=rn;6y0imR55+G&;+#m?d2 zxIr~Qbx2f5GzjVhYIOc@z4cbBrv9v(6dOG-KMBb-*IsMw6n>6{Gf5IX*DJ5Q$`BB) z72FXfNSHvlT>TS{QTUg(&I>w9#YRt10HL8BKo!LMp^J_M?2s8O6;ja@;c1b$&=_19 zda%DNR4N<$sIb7nZ%Ub0h8pJ;&4H#mO1N+|aRfc6yR4PS-Yofcyrrn#efQmK=pbu+ zAPe5k{~u*;q8DF$sU`F>7+dQ;#@+G=wh&Qy4hjCu$44pe2+QYNv?l-@6KyNFD}4Ut zDyPf%>zYaMmeR^nR z?)147<#eq~@c6edCKwjWX$`A;*>XZKpG#FHKRNY=>>NWgCMTPrrd>;%XdkK$#rb=& zM7M405`Z~EK!yOw;#^*`>fZxURs6kf0URbh%14aC!V>;u9>kH=rIM#&yh=CuCYfZc zU)+MS3qK;zOoPbVPq{*d0NRtczN$-IuF;B=D&U5!v; ze;hH>J#uwCHUz4EMiQTV;z`pbz|e<(Cs2<;&L+pHe-eP-(?yRk5+6XN%K}|<_dZ62 zG!X&?0H!?n%mK5{1!g}Xp$$z^Dx8A1B@6y+7N&ckn+qd(*a&m|NMH!61Xwmi-g{3X zJoYQ+Fj55i#>OVapPk3~3HE+)9@xB$t-o)tp&2AMA89Lsu~1Y1xrYRLS`vSIH*Sn5 z{_#9eMbR$c`2hCky2BBmwLq0)hAH?4=y`E%Nv>$m&{BaT;ug?#9rU4*Rh3aZIs7LJ z2}sh&Lbbi6hOWl=1z|$cSDN7}yZzt@UIhx(97Nur71C-#!Nb z!s*R7-(p@9?K2vBGHh+)KW_^M+HM49kRs5i^_rPjrohKDgS0M5DssVBD%XT6-IJ4i zY|z&i?+0qYNGyep=>Vh_fPYp@0Ag=HzV1P_c!29@4Z?fg8e9AKqQJKV^q=RIfq?mn^_neQ6{cf$ImdWwEzLMTl9pG?ih4{cG3y zsIh|Td|YWNoBN`sGG0==M$va{MpDE9itb=LO~1zOI*pU zK&9!%0!`W%A)WpEEm)zf2P{UQVJ;k2dy^RTIgg+nlSJGjL5*bRe^lS@XO)11vmLDf z{*9lP{qNzf4=a6-8PS|#3yr=tf;(}JeMbSc<=6`6Z&|$&5cgaE9ew-{)ax4*f(7b< zYCTm8Wb?cy**$O}0TDuf2axjVynK3XkE z)jY|sox`2XHJ^XM`4-su8TT6kC@?9X3riV9#k~SJM@BTcA5?eGC~1(GG3^TbV)FTV} z@wK`6EkK^KNLjDf*A8JI4>&mZP<@ZE*y~3o@W9@AMuD)%9f1G~Ih4vD#m*youj7Z%d9h4_pAvJ zgsZN)IuZ^ON>>Dhgn&^%P@$7(kjzujpRUsin?{!1ED`{BI;CB}es;8}qsGxzP=>ji z6PcB9Y~#JGgv2-0ONBj)sR7oAJFadf^CZ?F2{r8k_A|IrAZ{eayvLMQ!skZb%M?*% z>G4q+X&1;6Bc)Q8HTJAf##fNh5rv~l9{&vs_Nd7GW`%L(1i@N0m?JZ&My?miBx?}i?a$zrl^6ig=V#$l z5gyRFaWZuJSupwutn|yG`d!&7F=ah{EsKy*xj0t#7pol4NrD?TSHGSa^z`zed8#~E zDD)fod;%U!a(atvS(Y9~ObxEI=jn)2C`<6#@YXvNU+F}24-V?>5p*(SV>b1$EJ#;B)Un@+i94M`UXqe0l^Ti zA)C}+&3gHe9Y09`0z5YMH;=|k-G-c7Vs$yQZ`NLEbDrEUHZ^+rRAihhpYh>`@7uS?Fib>mvn4j`$+G>J_bbcSp3;`1+Ri3Fg6wM>}lh83{=OIt!BaCFt&%aIf_NK8UGpTh+vy4Z z1V`2wlLa6P{5S133I@0~G04t}<uK_;AO04fs%kW%FEKY1&3w?xTC}6g=;9pbu3t>t+LMGJw!eZg7e(;x`YDJhz zyrnzGzkO>w$s$IMnL^l>a z}R$c#}XNcf`x(eK|_BJUS>9KM^XXd z6ut|{8x^^?468mMS&~a7&B!U^rc4akxUU{#$VU#-XOWci%t5S0L5}NvMHN8{ zX&12ecwY)E)??={vbE~|^ETy~V&HEn_}9HQ$EQ#x7C>nkF2#QrV}Q~h1Ff{fgH$d1 zx(Pn-E%?Mk>$MwfPA{EW)B(WnX0C34X$oLemRKb868s?>N2H7oQqZ}YzDGb-rG!CJ zUvHOJJrpEQ0Ccg?r^P#)Ia>&LI9DOg1H^09a|Y|RH)7UR;RJq&WrsBIvKY?O1R~c8 z?~6Nx^m}YRETCwd@scfa7BD0}n`Pffs$H#awv^l{uX6Qu^yCpVB9Vb%%=KU<{+k313`*^Hg~&Yjwz}D0vrlpXqWRd0^BP~+#~Y`JuxZa4?na! zSSlhe*TLm?xGn@^0xX;6W50XMHZuC29Fxfsn|H#xL6RjJFjT?HoyB~^qY9^5C)t3d z4+)CRAI367K(x!*Fo7;f&bX%B(@QVC%vuI-D-QxSMXG<22_&5%mf}DNDHMt0i!Zs@ z?hDe#&nOX!=LEL+da^^_ler?66=q(wY?bWSjJ631%`%ZK7@jPa)m_RSc*1HhRG#le zsK#(wT((4qh-+}w$flrp4?g&yj(Wan6|o0M_qxx?rA?aszXBymt!8~wmR3ov+wNEw zM%dNB310}Lk7SnV5s0Xz0aKZrFs94D!O_9@gWu=*r`l-))(rcAXyu2Ya`gX!0hLDNRIhN_n$FWI5wQ4A#_;+ zV7dW{O#WH9G92Hz$*t2l0E+|rh`HMm(<~v>ai!(*2U$XJQkF^!N#oMJOpsvLJo4uEAG-%QT4-#ZB7hxoj&oDw^l@Jv#GTQjov)A`x zv4KY5kVm%8LMA&$4XmwDvSV@69)3cArFwu%00e&4*=I#_ob_(QVAJ88K>}FxBuS8b zm_ndZrqbo#O)w~JG$Bm+cbDWUt5DXX-vP-Kn;GZB=7$=ZB`k_SaGfC8A(q>3II zBOOWPNGkd>lSVm--`i#r{Hi?*Ft4f7eFNt6Er9EWPdkEnY|sD_2l}XtOxZMpUT2a5 z7d=96fzM*(Gg`5(8#|kO#e3~ZNI-5sJKf-J7BGorD}&BU2mh^4D=rBBOY8HGZIbe- zoc0lS+Tm0)+&fXO-f_I19!j>7Mavn}*p;^ZIQIU}g!=d#dsAda}YspWp)ZCm>Km#+@f_gBMMF9*Ht&2A>6LNpiuO z@xbmy3wvm^1AoN|6_7|3Wx zZ@#|9d!6Rphe8WrP=a?<0Ve@c+9?ST4nRi&tmYRkE!5`-gi_lf6w4-z+JuItr*_l~ z+6OW&AXeQQ#ez)?CfiNc6%yE)TH!(FqIyOoBeHu-uW+lL^>^vT0ol_NW4y=uz_9tX zDvZ{{>#Y)8?O~wJaICr7onQnck^mMsnR4guHQmc>(nwt#ecNTkHpUBCj5pqJqXoJT zG^mB12e)LsayBs*JeBM3AAih}8IB2-JXyZU6sNX@iGC>L$w&``M}i%R3w#Jv689@h zewNt6vosSu>iX3mVM^#q9F1Btp%vzyaq>I(%t5QBIhGa)FbBqFY5Py>ktTwm_2Bh9 zNS;npav-86Y|3UYtVjyy#IHA_L#P1Ol*vQRvCNv8*0_rYiVUoO8Q{9uL$Ngo{k_e-!Xd`yKVM^|F z$(F$+K`~4yf7e|Ay;o0=dR`}h?z#JJ3sMl;W+p=Qp(UH{=GbS6llWe1qoNl@B5Rt) zDReuUj{ah(n^3P!67kc0)XSTwVTiDCo>?3z@T}+_3&jFOJ+Zy1>kMac_R1mMY2GvkxwW1 z}zmsmiP7vOAu^jFd*EG z^No)0c4(61?Ez_X2`|q=MGK&d{ArY2R-+U!e^(s+{!fBN3P4#bGc)_+a3tHt$m zVo}HClBv=m^a#SC#s-%WGmhGWv9Z3Hs`YKQVH1GKcCpIkK}FS#N_;T8Z5zp^RS_&? zdcrJls#O9E{XyR2u=NrvbKkDd50hUL5XRlikxMrSGG(esADJ;EB6|%|y&^)y;2$-;Y z38Z){yrC5Y=ZDK68xCZ~d_rKX7a-jzNL!`xu<`yRB1|Cg*^DG|oeydtww8SN3tKSIC}pCJ)U(> z$Q=6ToAr3Hj>=x9EgvJ3nq98jc$a0EWVt)*h2ZKiK1n{wMmr4>%2bZv%$j9g!=h(h z9ixQQ7|4%rQHoHE0Y=$v)-aGh2>|B-I(4e@4a~JqA_5Sn5~OVbFaZQ|#W$Zziv#sO zv_x8kzl2I~Zf`33MZ&=J0-|E4`8tGiG?(I<;U55^2(osnzV2i#5NN&lUNW{f=Lvmo zt-H10Kk{sb5h1y$$LTW$|E3vF+QZN}s4(@)qY+LB6kz|8^ul@0;O1GE%zUPjp#GV~_xTM36uA|zC%gF-FS-_pjC>~Vo%Sqv}g zL;B1!&l+=;K*}PgIwz3k)pI*Slr9sCm?VH%cpyGtH>y75Xx&7}8NE%#r)||x2C-mC zwoIi{Oa(;%>K^HXK`=rA~E7`rT|)j6%|z zeq6`#sD#ebb7A8X=>n6D8l^d8plkvg#|bnD)VWK89UE0spbjz)Z&mttKF-ATXY)Gm z-1DLupURse(8K1&_3`neND!PYmHqqujGNGATkUboNdKY<0W@kh!kU=HO6oWpBaD+9 z(LNO(!i8EbLAZnzMH0zIN6(e_;66gEq#w(MN=pWW#t4Rbe@GbEV3{`H8t3!YvW&&` z(t>@JiaAX6JEf_4`pQ!9!4wQPA{HzaK@X{jVD)86w;&G?`5!CxtW&{0@Y~;75(eLf zwZ}c7RmR$h$dU}me;EJVe9J8+fa5%$P7o$@SxMK_ku%7$Z@dxp^D(D_1n91Pwe`U< z7`ae?_t|F;+H;8L6p4e2c*O~&EU%kK!yzVvVDrOi`7pH&=+l$f+L&Gz&}S(DAo<~% zfHZ(gAyBj*gEQmWbYp|tR--r?C>RL<$r1_r>Do$ztZST!OfZS}XJ6p?=XvM6Pd@q7 zRuPb5V-b3{3@dio;6F+DTPUQp82q0c7nCuf99cZQBBh*O=o`gth4Cnzsc>fYmk0i$ zdQOu8ItJJZ0Uex)+5vy>d;j#WgDHU0{?`Lj7;t`CwM@yU8y)ucf0Iyw`G-=#GL#pQ zp>?{91R%x%O*!lR2!kEcnR@2ZuhelHHhn7a)}5{=s1W2o5bY2jZRC(;E)aAN_Jg|m1qV_zvp0;O+#B3)%m^+{lSUB}!WeDDFQTrbj| z9@+*`n_yP0?i}hnUUlyy^Y|~8K)kQj1Y$#Megwtts6VGV_=8>t zf+p6w1XW!EVJ|E%o*;CCu@&X;S&Yhq=-F+u_wGwg`T z02?;%K~fm~Ax^eu;gi(c2Yq0!+Rj&xfiDt9eh$RRCQBfHUyq6Fk9i9%0SK7{6`n6P zRnU2w~K0}$s&J!L;&75NT1O@&^D6b{l)?4S^$#T68uR*!)qxJXo2E} zX=vqILMzkj*^Ah7;7E})*z`jh8-(QO*L5`E&3oS?ihqk3B~n$ylgYIb2A{KdF1LyX zOz3I&?0N_biWRr~Z86fwc-DeUa%p{Od3|XjBn$HGE-$@4cH?~jpA`rh(r4y7t+Evy zfX#Ju02*6Lbv1!o{%twK0fzS<6fUr#%=ZW9jrb3V0 z2!4gNkfhICF~gkWj*ERpAa3TW*(Bi*mFkTg~mBmexc`U;TLL-0NGjI&yiv4NVbbc8qKxahNpDk0Ak zQstmv&a@v$4!cD7&PX`?UrihYj}t}kXeaUvN}AL6fa`l3Ek6jbBo2^QDHsUW`MiE> zXry0|ez6*+)j&c{V#1$m$&az*K{43V*ndtt;|#m@j*e{25_mC5PbPmMEZJzv z`_0ntgDr?aoz+5eC zq=JsYY1R_y;1w|5!)O~LhHP9Lo9j{EyQdA{#2t=>BmxIOL~fA8B?%!RVxe;_Tt9u* zjzmV5Fi8vZ^dum6|LRw;@9{MYhPJyl#ViHtO}o}q66Y&HkGlyH@VIX5 zY$#`)c~;A?=9rv>xoGA%WMhI(lH!XEk!!~}0Ht#7J~GAmM1i3bE!cC;ZQ$2?=brMf zn&H%jkq3D{cPZpL1MTvAkzn|izTYv;Dat=#B7^7A$)F_yD$7BbeY)@h%U$^lL_(Q@ z`(-!}6k38;1sbgA3&>U~|MYK`*ejWrWR@!QuAl$h$h+3E(TevS71*))czqq?tc;gh z@4cNaY1+yc|BEXW6+{7W#gshP+U9D164?Vh0g#s9qmMjniSi9M++Ywm&yB605si;% zp0#4!ktcNA6h!ITYp=6)#rlnbv!^;}ExHv;e&OSQ0MHJ&%vy>25^|7f!Hb&#t=K3u zTm!0f_A5w=AZncyaGU~CBA6W|Duc)jGY(6_O|e7);K-FHUp(J@9^=VeZ}v&9{fWtX z-6tq&$;?)QzsS~1e72<7AHt-#{4#um)|S54#*hizzr)g7o=*rZQ#$L}7j=w(%|b@> z4*de#3-mb*!K!vZHddGpew$ALh$aBd{LpH%#Tp_s3k#S`*{xk!jYu;j$uv<0Nkl5L zO%=i8k-nsvSEM;t9(h@y~4c`^kL0gZ$o{^*Cz^(vc6Z`|0=kgbv!$fSgDPuJVXf$Qt7kq|+4 zC2x9o@y2Bs zST>R+k?0k|r%?ftO4eL)bChOOUm$Fu)RATPginb<$mq~uSLG7;X{&jH7%iTof}{8P zXK6B|Z~K}jj0Akv4rDhzAwEl?2_t))rjgA&%j?3p=^2E#Zao3#ltUXky8o`f=L z3JG;(HLfZ41{u?5kX#{jZtH=z2MGW{+qor$XlB+|7Z)Q+iAP`Cz`1Gjlsv>FdBqYY z*796Bu2Vmt^LIxM49PpP)^egI`7X-_|FK{`Dgn6IhB#jrvaBb-0tL&^AdHXb+1LK{HpqCb5Vy(W2n z96I!;Nchvf^=YOpv1%2p_o+c$q9W!lTgQjJI?u>|tfQN5y2*&0PC9K06srq1PDth7 zKzc|@#U@tHFtn!|;R~($UuPN=ZL^J-2ag09T{t+Jc%G}zH|P*!htets*10S^LpV_~ zn-7`T2)X$@JxH3w&*FUERkuNo6GVjPkg-+s0!S!Y+X6_55@Ul92JyYXCj^uMnef9& zLDAQZPsbpRMT>#9CL9Px8C}buZ`?EP543n}G%Tq2mrFFlWGij!V}&x*$zOmNXRn!Vxa;#%4Fd7k;nvXFcDh_K zi=X5BostXbLlZ20afdLH_@(Th1^0 zOU@l(^zA}5hPwXx8;lvhZbmkelCd%ca`a()s`Q^xJ`q|2@PQ!pqW@4EYs>F}!}267 z1ac4zdiW#{M;Vu4qmsCH1sI79dIL=5zluX48~|DXx<*I<%nS&h z1KlI`?%UUT9L#2J2DFm)w-1fyLty|i0b$w-m*m3foEi`a5Ib6C=BTL&GQl50F>AEl zgL1@RL9iV-@fGdw&$D%qwVDm*yyJE=%5TNUV;jn7VpJ`0;5jR)Mu~3%d0+%JS($H@41uF^X~*W z&Iv;jHVXc}*VF(LKf=UNLOXQoa3ALqwf9fA#NmbKBdS5q6-Ga*V?gYN9>~01{=-bZ z?Fe;SZibv#?@{4&nDq4P5omf-^~d=R+YUQ_1XE;0s}B^gg~r4 zvpwn9JBpQSP0O;{%b70ta@440I5y4J35K#P(MzfcM@aY2UFaZXG6wett%BXb5j_n0 z{E%X|r|OeL%kB@)P#SCX`{UPy(YvdzzQz*g$jsm9S_GAAvYE(ltOqQ7L38-siZ50R)m|Lk-6_f>119e={!gfPvE=d~aLo3iPKwWmlvn7O~{rkkU8 zz$b?bE{W*?Bn03c4g8QH|Eg=0=a}Q&yVnzd&B8p$695X6KfV02jr?Az%7|S8FmI8D z?WQZTp=iM&LsfY{OPbG)T={m2hiviJ>m5D*oE8qR@cmJhlco_1%TlzM@A{CP50Ke`5 ze=R^3dMbFKs`yir1DC}rW39WME<6;i2vRS;@S<(NTy%Hs=KZNSXQtgfzjG{rv(_kOP1vp`^WpT`R2t5&)M@o+pXgal~FG^i3uRxm$1`8Xqc@X}7=Xwjc|RFZU~* zw+Xm2%O6wb{Jr&BG}Hn>J*`M@@M1i>t?ILUut?SVY00?r8O|zpGw|o-mPCLJ_}(FZ|tw`PC6a@7mQ9V zX{ypx3BWQ^nuVpsj=V}I*&{jT??C%N{t!<|Heu~U!hQVl$BnhV-{h|Mr~uGAmQ+hh zwj!)9Em{*1wz&nV8*jYH`smz`_0+<%7GS8^*Uqe}M&cJe43b=)EeM1>kLG$gkJi63 z;ui-Gh|i||N1{v+CjkIHZ(E3i(MYji&U+KYX#u?b&RfkHnvinrPQ0CE)%HA143VrL zH~g#`R(+&xKE`Yz(7J=Adzs{Rz#r)|2&D=cPYA!8Nr>7;9&JX!ig? zEPl^Xm(Id=YoPAJ*KgTMbGe6mE5U!cL4+|4gq*lJ{E{Y~=v$Y;e}Zk^ zvln&)5(o|z2*H5LjIW~=39;ZYm*l({T9|ZlWH28aT$vR*0wxcv?TwSt zMh8I(7#XyK08L_?IqrTpM{}`gn+l%%rkYKVQL#(p#EQyIzNK*p2(syk)l0_z;e zLZIiIb0cxlCzYD)nd2N;lURWxi_G!ytdexVS8#Km90BNB0Eeu=+1(}pB?rJqlKFl1 z8ay`Ovw#3_J|?q%zW7RZ2g|z zf(e!E*lq|3m04uIZ)l(NkGA2lVbZsxm(Q^PTY6nE@KYF>An@mXo~mq+3o7L8=G?e5 zStswdWB`r;Jz9>L`-KHDh=>XEK=x`O*TpbnPD~^zV8iFVV3(7);UY*9c(GBRU0!!7|cUX%BS#C-(G=&$)tasp%+a?qGjEJG=iKwt2z{%+u7-NcBhs@KH}#I00|1jKCUNg z>FSgKe4M$C<_G|&1j!6&Dn$qK*}F6WIGhlG`uzJcK|J3(>yo;mH{L0=S^wg_q(B%# z)!J98e1Wl_g^>h8S{7d3&Y57w6?!0X5r>g}X&M8R%tkn0?jf`UyOz6^84i~(=Y2o` zbc#*ic78w~okKAJ7@fTcL*%a*SS_I4Fy1C;1qwsc7d@)cAmr2ggXarlXVR)>yz7nE zBioe?S`^cp4-y_V0R3K%mIMaxcuOPlI?AOzK8r28JBDl}0;$78A)h zKKO|}7eLZfR`fsVLlYE8fJg+~$G~+a=;ive=->Gy!b&1v2#Sx@0kM!t*YmG z&}PE{lcRay{`-yi$~_0h=epeYo8M@~yWKPv7=3faPT+G|0-G8C0`!L8<2jFspp1yt zC^pnw?i#z(C(T7aSA8gFuY+Ex3>puZ0PVF6ij5OUohlyGC3v&~vzc7N;6KK_h8m zGzxeX1cdekQy&jL@PMhIkz7%!K*ls~fmP8RF-)YJOBm~vdqUvjm~G`28mqeCUtm2= zFuFtuneo}$7e^yCS?k-;<_TS2$cUiv7p5MxHSoON3&>d?DMG{Z3%>%Uwm*+9A`-1( zH{0`a?trn(WnQ!;EE#Ek)@xZrUGn~%(x|0xsQzqRcj}&b6P=%;hY{YcKh`X;E1b^6CUBK|%|FyOCOubKf{;sig#t1MP1y3qjk+nbo zr}{rCDeBF8iGF?`xW}wH=KtS%`z>od(90*#T8k>V2bQX#E4B_vpl}cc1w2cyAqbp} zBO3xRFChIUIb1?A1ZhAS)CwbAuZL3$pq&~>cIsSP)J=ItkJq(j34o%7-j*oz6t>jD zmj$hZB@_Y2h=ikxo3b?t?#kzc0$K~xc0+}nx$T)<&nGlpjMjj-D`Q}e--dRPwPj91C)JSvgc_(N*tQ71(y1S zZmamTF+y77Wlul-l#$**2B-*V2N0ONCs!#y4cg{CNEqlH@*ZqfFmeexe4BdQ;mDw` zWu=>mz(=5^8YW|YSs_f`2PA?3<^~-h1-GG?N@%P2%|u#b)R82FJ~F*ICILVzyjN+2 zq1miNI8s2f^HN>`kowVkB8iSlim3^D)U*NU=eye4fLs_lWP=F_5+o{e5)h6bxDX~Y z$H^uLf&!GvF>gzTMtOfcjOYiHov8SXa39QrUH`}VpPI%y`FE1DI9(wLhW(47HdHmV zY_+jS4>SP1Wl0@X7vF!?{wstq=E_l)vmSRMbkQmxKt#^V4r9Y_ z%g};lzIi?OB{Z{E)_M^EH%yGj-?!Jw@Mxi66JDMuwZwR07(S`pv!ZQPWWD zT%nKx=j*6cf#c>$ONMq20eqL6@U#k^Qs)m~s)YuLR{oDDk&(}2{P1LT70ry?MiOAL zRuh5tcaQzf&gqg%E;FPsSK919IKi!CiIr)h1-~pjH*9 zsZKkU>7G5?M~Nw&+m_0aY882&&Wc~Fz5sXq@-A~CJNMjkY+NkL`;I?2K3C2t5dr91 z-};tq^2h`~_|Su@t9`1%Z&%sBnT4s32FkQ8Djx}Y@a=DZ+o0~p)c=4VdNA_fKa{JC zmZL@l(yEh@C8}2leQaJ&J^hE~$3OnDo%=P{Tw9g9>;kur7jBywG*8gynq7|JiNd&y zKt1xvqs`A%yPb{wmRoNzNU~-ivdpH@ms#bD>*cnuXQ{am-v+5;_pFnqX<-v2Fwa~zbFH|(Iylmf%INUN;YrUG!(#kr<#NSbyLNn8b}}~ zahv*LBWBzaM1cDXanmOhxWB;OB(fx@j1?MLD}y=>38;=i^{W;sTc>$KRJ%7a^c$%Y z{-k9JQT=b&Bt%rlL{*aNo$B`QzWGgS=}`Gb0^t4j8d&pR{Nk5POpCSh7Y+WWXdoq7 zCD8p*1Kt&C>a#=t8??lg7%h2iwyIQx>TIK3Wv%c=;^EA4-r~+?3g|N>SV|Y#cN-B21 zAuJ(Zzr^u{QGqL&l*#W=;KOimM=~_K>+a`%xy2lSROQxdW_lsTQ0a;;D>G%a-z#j& z&O=S5k2foCcl04N;6bW(AKh3&aAIJPN`q2{lZ1V4Kv@0>@#}=@8jI*=tt9B{J*LDS z>_o4-KI+=fl0N@@Ko062Fi-*l>sRmiYJ?*7_aFG}17nH0X zrjj7>xc&Aon8F~gg+I|?OSOH~RmT_bY{{F|>NR)_cU9SCgI5Cf?+upD5`L!VepaHku zdYc^w$pZcK8Apx9g`nMy0Gjt^Mgf_as5nEFH?LGZG?juMe&(|&st}7V~+!vQ|P5sa! zKx<}b6wx_z6ii9(Y!+&AvG%-hoxQ^6{^v5tgNc-6l*NDB$TI?GVOBhI{bYcrXVzBv z^N18>(i4eq3ViItPZiau~{NM*aXzscDp5{c|$X%v)B|(0YjV}57uvxH}F|J{wqW5n^ zKmFNn?!8z0vp=^A<|<`UM}9(-TJ4u1F23Td|7f1F-2_~)Uwul|2k*M;7do}@Vym|g zNoh~2TzPFvn3KgcpZqCw2-Jy!{1$!xYgIgjss`C&dg_dly`WX&v09BsOVv#aczpKu z1c4h2o;V$`g3dR(kX2xvc7s1>eq9T*l-ReiXN2SrNO>ipFNd{l^wCEV?x!{N+M(C& z?;lu#@PP$0Q%6ki? zNsyyj&QH5JN*7ohrF6B=IH9w{o`srRd5#A7gbi)L9Xp2!!*tJHvSOt&?mMzqPmS8u z#zjB3;Gb2|OTE2JnfSQ%3o>l|IKgiwThm_zOs@a_lzXf zGCd{r?>A#WA@J}%H)|jC7r*$01#LF#?|kRmT0QTwAi7#{*z0s*BH@VtW)ut&ZN2koUZfKQ>+4-l zae^!;IaJ*v)8@JgByIpzK&ro{A+Oi>hIenYgpg|c;fEf!Aj>odq)z6Vc^&XE388(j z&l8J!o-0>$LS|Bt%L!7*GPimIg_SqQrPB$k$Jr*oSDy!z?ckC?mL%}=3_t>8RMV0w zeD3vlKTHXN87Zytx=*ZPB>>R3_MSU)y z_4_2{vVe%0a1W!tQj=SjT*=KQ8=_|ZpPDx{32>An@(o|hCZafj(DOqcVTIluW*yHDh>Uf*fHm)}6 z+qe{yzZ0t&fzf5u-=c0=Ez1S#9xX3a^#^3skZauHw+2nuF3WI}=#SfmCX|Y0v!);g zwiSFxrac{+BR={krLj;;^6mj$Sqq>EtGXerHockPFa_Z>GEQ!gEsBi}?F7TpEu`wJ zWl{|)!rjr$>y}$?wV8ZKNPkePUi!nZthaC97JFNTK7ZHicC|4`L0|vIH*E8w&wQT> zk}wj6jWBxs%rT40%=Y+@b7yWFvG4@zf3k5Av|&S^s)9b(sbLA5VHCnyYlCgwURO*T zpN)Wp|D`W|$=>@%YTf(8AN{~Kgs=*X?RMimu_;nJGd4j)-3Mp0Q?qk65gS1c$kqA7ZW<)3%5@;Y>{Y~2gj4)#MbHct8 zjL674*c|`*dR1Duw0-SeleD?HRWTJkP%z|3xDNz>*DF{zS=UYFhP+{c6uk##&$(VG z{{gcDv3oFNhI^wW@Xg!mJV$#%PwB&+J-xOY(vu4ceragV!@c11>Ye>MKP^LVitORiM z?r*#Ob|Z5B_<#P#82=>JsNk{w5CS3?-}Z$s*m0v2$n%NpEu)`&opHulmdHUK+)Q2# zZoyr5ON_njXtld;!I$KosSeoxZrtVY{^$^;jn~D51eD`1Z@p??I^HC(g5`c}3wg9Nv>yF0r z&q0a*d z3AuFj)3yxJt$S4p$3NS^`hEO9j34@>zO>;DZ41YOzBGN$q0)3H3?aJORNE8@F#nm) z(6ed6LcjIaTTO}WH(Fslq?-`I`{rA2ww^Rh`W)uXO*%*pT$PTBgo>L@2%mB)$5VB& zo6K%*GSc^APa7_kE`MI70wWu`Nio6$VeLmh`jIVqHbXyd*PI`dzSy=JHwiYoGjv>u zJY`!sMPqH?UV)Jb|wE_lRz$TlG${$w8H{qsAN^8lmz?z``9{{8>_A2TE2{av=T z7+>l43^TGG*Nx4MFWM-yESM+$tr9Z151=2{ToXsOPSs>!+v;i;@`GbQ5v{S)qM_wX`OI_iN$| zd`|*S;tHgH#OD0()ppGTcHO`Hl{;D&Oh|60>nJVBB=Wj1DyM+^v|Y~wWC?BuNd`ng zLty63G&4Q*-e+k&k{GUQu!lg0R|i$}hh`BisW)8-catY!g+SpUJ z%;wf3qEs^fN4-BXf^7)P81P6%P-oKIcMCB6(D`xHJ3|)uX~@vca>|sb-OXemKbzV2 zmYlD?RgSb_JeLV>?@1WDPQ7WX5Ss#tL{)di#EF_ z*<3$!w&rcVIk(NZ+DA+1gnjF#PZjs~|L~8t!J`pCtAZXd)jLQBng}d>_dD-jxQ%3` z$~giF->wxG*X1!CO`?wuBz>(0Yl0|K1MY$sdO^3pk!+e(@~TziASK5nHAWH>Dn(>q z?^WIb?|G`C1yoR{oN}@Sq{FGgF%9;7JTKG}>5dGOU|zj`o7VG;0gGt_5EI7B*r?f@ zse(aZOngVL4Q&r6K2lLblsx&A{gyDEaNG&a4n-+V|{@RE5xFYGjPdI4ZSav+|rj&ykDv zaL?kn;LOJOSWACZ0^^@vQ>$#%pC_^Aok1j!q$P18NJFs2)e$WQ-Vc&0EdNJ!GUU1E zp0Q+rWbkH{_GJVOvL3CW#~y#o5^4l|NPHepO$id@tM&OMDLwS+A?vf-Lq0HZ6?M49&W-UaRTo>-p{2zRkvH(e2-%P&g!2-Xr=$l!||S!7oYVKawXB zNsz^jBp}A|nfOjC&{PX18QiY-kN5c7-`+9tA$UCf1fYa?Bg)9X^LwCKXd}Sk2pLK? znDj~rolP4ka^kuSeFw!FXJDR8lxlhkgL=3iz)6J@C%Is3bw3Z8m0TgV6Nb$BFsU#) z#vzai?0JZ6v@G@!U79h>A#1DT&5#hr2=5aXIiaWUmZJ)iyZZL}v!SB$wOW-`nv_M% zE{NpaiDHZ@XC+EcB_R~_fr@pTRCG(L9strT>yw&kbc=0LjH`@c<2_lBo# zu(|CWDRTx|Z6!B7+fJ%jE|qz)68Sba|3PPC->}LD7@unGD_{OfocAB=U>NCV)ea2{ zPd3-vlvqgh0CbNWDkuZN>0E`(XH>&%0@BpSZc-1C`~80E&$F@G+&M{rzTcg9{=_zT zCLvtf)BB(Ox#rbWeSe&cYS>swPzZdG0wD(i^Rj^uq}ilbD-}f3%RFPxRt&CYM2erm z0f3yzXdKB8Qw(SkFg8a5#cQdCT$#{GW)9tss(r~2vs?))?WV(fN7Iw|J11+kNwPzb z0|k0r0~>HQ<7F`K;SCrY{BLI(+{)&>Yftnbu&D?`c4loLh7i_NN2)sJc*t!$LtVBv zKEAXA%=d86iyW(T7HX+i;ME)%@M)NNnt+)fjtK()PRoG_7TP6<$b6vB96Sc595C*I zLqg)psHGtTdi}7GhId2d&oltn?RHI;L6}H(-+c4W7U&WFfs=8EPM)v^5OpF+=kIU> zNE9L8qRIznf?0N+MFecH3noE8+k=2;33JU4B1NHjLRbiZ$T1&$`am2PR>CWjEdGoZ zh-yW3G?HUyX2wIJvVGeQBe2 z3JAtT5k=Zaf=~r*3>-2cWo?VF9@?3&Dw3 z5E>T(>P7XCSqOO>kI!DMXxLVyoQm5P3Ou{VsA%B{38lw%=6d8(gR-DsGR?%S0YT+} zLEkPiq;W5fg39E!+i$ZzBNG9PAMM|NicJZS&A+R`VKuC_Nkd=oBN}&Xv)=Vh%5*=J zj5r>8=yV$~Vn*uSY9 z2AKZVK1qFO*Sue3)Zwq;^Fck(Lg0EL>w8H1s1H8$kix|GTP1m={+m^ndK^)F=!gaJ zSgD4A6lrX9#2!)rxT+_8UoUry&9a9V%mz5qjGMB&t}B!#8SBmna0Z&(396 zlA-24ZKjRWxgU-YINwBibN8$r-x$Nltg=*%P^6k9hMppyLkGbThSaKAwMu{063?cE zv_CKrBGli2?qRpnN&%HQ{`ljpSI&$(QpE&)&fnPUdJR9)b0o0B)JNG4&JBqaYlEu) zdL8SdR}abp^1~P)DB6wM{(=+md-VqR{qG;QYfJElV{qU=RN_Qc6Nnu7TkaomSqYqB67$QI)xP_DIcBVAwOn!9C?Ev$A$(piQorSAiYP2$K6)8 z@Ak#JVnoC1u2<3?(BDsf@)I);Az;%dheSl6T1yhYwadJctYS$_Ajy5{OJBB%lkq~B z{4CCYP&l6|+!tT0+Ur*?H~Ju}zgT7XzwiOdqFm@b>p8#} zT5@J01W6|$40OM4>V|8P7`YyakwD6vAC)loiD!y?Vi5PSyA2V7)z7m@)opY5`tPg- z?iKxgB(D!@VoZ;oWET$ws(zA2xFv=PXn+3?e|XZ`2A~3`=y>4cnqWSpB3Ya>BtRJT zOa*|-KqSSg8DSt2GsJ%IvL}J3y=CK$IeOO%{$4%9Qvl5%iNKnfw$ol!&x`FjnUt&U z^HHLf!n-#1$av$adTm&{9r-U*tIJr$N6Bs45IjS)AJDP@shp?z|1sVUJ399W!eyL- zh>DQT9SS{@EteDKcMd1be@prpe;x2_qyW2(Y-%oxG9)IC&ATrZG(yF`x~8J{X$gAE zk`cEai<%|nkFvmxrfAdg?dCBOHx;O6Or@YN$62xQ7zpxahFAHx!KzoYQfEPgzC=$m zIkQ=0Ba*}Sc`HDM=1NsqUm95|n{1Zle`aDW5&GKQ+@T~RHgH65s5aT~zo0Y9Y&i3b zMch{zgfi*lg=ecG;1@M;(=)#J-d|fE6ZS6w#4>RiSDJDeQvRLloC@fRN{AxZurc2I z>t8pgYgO!hI?vKqR%Vme!Fz{%dr$Mf{_{W8M)$EeZl{|Cq&ot(GxhuCK`qRfk_tDp zVbkaO0MA0$`mg``FXK~u``h0!$F`h|)>KSFawv19opBCvtc^ZD3T#^SpROA+K?kIP z_rG5Qz$Z!)h3eM5tQ(xd$FySCYL{8aKxtDJPweoM}HUf@b_tzu%c-IAfTl{HH!Akx9=r`m^T zOEPPg*!o=4=ch#gOCJOQO$LxDl4g=$I5iLk(TsRl(E!%YHQM)wkoS@%udwq;{4Txh z3M08bt*_&_u2FSPexHE;@yFfBW*-TW!ds*F!w7p}P<4gS*PCed0P}q; zl-z-w3@BZo9ZmJYL=HeD6$?FTgp4&Ab2&rFX#JsoS*29mYE=OvRKQ!y(8p*H6H;;63nmZ<5+kC|n11E3{4Rn}7fJ zCL)3(*DqBr7&w$3_wHn`>oT^z?YT8*0V&{^u1Lyg93xfxPk;IotHy3tXcXd_+YGyR zc{3sv>|=}Y5;(qZ{N3N#`y6=c>E_O#{?u6FpchnLBswL0KkQ$-Ivwv9)%I3w(+3WR zMgOD!`JoZFXceGyz`ucjID#-;QO#}U-pNf=(R)%725Hc3d8sNnR{Fr`39wO;1e}r* zE&^+lMiAW=wj|{BwFT-3Q)#5rB=X?Qj)5SwlIH6P79c_H3W&ur)i3uBC_1_lMtN6Y zTB>8)^bHA!f-RO{MkLS#Sx5-r71)TKR@U2F#2}i!#OpXKwlasKx#n((%XNawV$kh+ ztom@@^DkN&>&fFG{!R?~+*=R}D$^Ya66aY)t@2o#G2dkYoacx0V~ms0H*^Qkz^7$^ zP?l>FoNa6DdP9$JsRQAG3-I7W4`_n$njr~a(L@|GAS!=&0H6*xX`6tNSY&@`1@QS0 zW8rQf1I#&s;$5jp1oOm{P1xO@o%%9zwjIRf4rY1o5_!AxmN{NtF{ds6A zzw6J3CEKZp;kl0YbY4A1TT=*aX~NN}c7_Ssn@k((ER>p?FaVN;Glx@?dE?`HgTx?= z{7jnaR94AdtG7K|p_K5J{DL5ZsRa_{)ymn2IxS$rcqt!k z2@`}qX(n{TTZx1)?CW<+I!`7QcqGjQC@%A>8IIfAW-&m&py{YuA2>&9-AIy zt}YSC`|X|b9oaNh9+mbt6#55_gIho!lbLeJN-p%#Ws==VxJZokE}O;z37!KiNHqDW zh^P#r!O0aV$jwbrurCRkUwpUXr9$8{kl!`PUtvkcs2CGuqrwLq=Dm9wwBu=&io+cO zsaPOy{o_A=%kht?Ay zwpOT!d9L6Q#2N3mOm&Z79GUgoERcGVw5KH&#@4`g8=u=9C4JM}eI)3GZk{(K!0jYR zLU;(G1co^8y=he0`ZZO&17@l z2B5bzDWgS08|`%s=x#tmP$l>i$S+m22Gj_o4@?F@ z+KUKVr%v9%=kV3H>^MRhH? zZb0NFnNEc6KgLJdd6ccWNIpE`g0DS|jJOBUn(%}Pz6#gC-BW88ZcYMpC&Rd=!xj+O zdf*<9G2!ITnME#bztkx#WrGV{2F|aRv2A7m`2+m6E6BNqX!ej%SXH?SbTlCnRO*iNk3mz8K`1 zL4{CDbU60dvI~ZIeEU1!v9U$w-hc34|7~Mc$P!ZlQN0r2Oyniah>z)s^CtldbwvUc zbM;Rvy6}Jh@Be5|_kYdB4MP`Z{2ktx~; zNFwjcSQw}P8ykW6OeKktm;gPK-0)Frw&|_e_k43+^hKK{)D1(Vh7NT7cvsYOVV#u} zpj{{jNS4hUA|_t;*uwIBaGb%N!BRioZK*eu4NePTBgx0o?g42)FJkVNzVpuP$H{y? zO6H_KJ{LASW>2xgo0$vbEFEurrX#B1a>%@%|28K<#t*l}XO&79^Z}HJsTa@}AZrqu zNFsAR`54To>IvY?_|scIP5a_FGlH;zw(&j$`P**0-GVu$xb>0{8A#GJ#}q)KI4 zYkaJ()*@b}B_I_U;)$liKx;&%IRQ&Z)92F&ndaaS5Unx^`=^w_?iF++vIN6GdAM4C z&6^P~fKdU(W;m}eJ%fV039?nrhV(>$!qNhR&2kQuQjnx1&Gb{LFmA`ZJDUq*cq9T; zLO|L!2R)NCgWhStdRFBQVgwb$fFSbG3b_03UzyA)8{@ZhL6h0DSxOR731QZ&1R_ds zB*~Y8Dgc|3<==VdohGY_Y;a^fUu|$K8&+=jStka@YJq+_RSv&Pa3H|Id-$bl^m7hh z{_>YCz#9CxzEPbC&U@>J6P&y=c_e_Eu(|&F*Y_wIaL_g|82t#m!2I5wtc5pIZ@uO+ zSnJiTB?7AgSrCjBF^9aaUq156kPwoA|({K$r+&y_4XK*dyhSAOt=s#`77uD`@uWZ^WEqNMasOr1pHRlfdP8U? z6>n=*uMHeX-x(B=%N!-Z3>wX< zfA^^AD;!X%WT0DG1&nEN_2yx-BiH!`*CUdYV?Cw7ke@?f##)bqzU?*FU27w0Z0t5h zsC`}9IV5ugJ*0L)IDlwDDvsU}H4-ctvBz`X3Y~OwvbhOGm>`73C&DWy$%vBz-R_lz&JGdi3Za`o(K}dQvYGtB`-CoEWq+c4{KT z2;j}P++tH7OaUNN1%KcIH3`Bc^B9$2hVV(7CmK{m%WZTAK;tggWPlbEqBmxuvo@$p z2J3vD2&Za?Zb;uNFTY~vN536TL!8DcNrF>z;anpEC{qcv5g(+F$F2Ani2n%8ph`q3X{fTkr{(67sFM#MjZ^C1SmdlLcjQaoS7|%E`j_ zXwf=I0D9pt=?Sx;eQz8M%q@duFgo2AsVgUg#&rxlZu`9cYE=wTI)Q?LBUqvTQxCK? zq+l>T(50uH%k4n!sV6+V1wDY`xT^xt(pKH;i6}*|g{%ZD_|u=>X*Q&g%g#RMtmdSn zPHF?zT%@AW6axiS64e(QAq$ht9#-_vwZehybIWbF>fH1m`@3b-K2p=m(pki8*1*cZ zom5l&oWM-~{=b!ZPe1qCYhxAkNh0Uu?^h!Zah_{yn+D^tfp6RPO{=IMQ6k^(^n8Ju zues)0gJo$$_$0(;V*ZK@bpkdwWB--^j%2ap4N4{<0JT6g=Epz&zs;FiY0-iJnm<-& z$a(fnc`o%jbF-W=K_~v#TIHs5Wh33QXPGr`2?4vy1cln{DOoMjivLjZjzc@+#ef`#hVv2^Ddf7>GkRhT20z zno{$tOE@xl;Nm-}{Fz;Bx<@8hw30}Mv&j2^92%MI?;4vOy@L=jJ}(M=!`q_VwEbvELh|87gp?p3r1gCBxC zus@SIk3Rf}5dl%s0}bGLGSa68TRa7@@#Yz_=RFzajJt_+33436Ty37!<9bp(2v|25 z2k{J$=rrjWT-0PW;~#x5euuUFwkDhxswf!KG4#lfRG|A|uo6omI0up+o>RsjAraQb za)&b8akAij6ApUco_7duml@PqSR84+>EP+xz!_QSPGw0t_NdzP0kOX}F>wmMP z0Mj5g4R``f9Ypy4aCOYbt4ChHmgcj8<>3UdfuR=2u~7}NfZ2FRD00=X`o`g7eXA`2 z=zoz+J3opHe@ z6cON>;Ps9jGS`5OEOHH&5bGEj+2foYE;Uk9(m%LFxe1btQmC+CsxwOV`s>dciIM9| z(nlo<>H%`Yf18)O2|$(jUheQWtIdOt?FOM~TSv3nE|yQNBvinH+hi3Y6|T{^bIWXH zR=|mynK=>97uSQzm=+jNJJ%~(I&ZUQ1N4TZko!gdpX!_uKdNV7dxUrx*YjZe%Aa1b z01vC59zK&bw`kQ5Srh5%#~*v#o(Tko2>#6fv-Td)mejrX-ed2>gbJt*Bv_Cy5=vD1 z&OiS`3*L|%=V{%_g38b*9X?bdB&`dQmoqFOJvW)=tfl90IEPfLqTKcx<~|7$!atxP zOhs%A?1N=gqMR1qpa1-t$tlB?AOY}%ZB$!7?HOxHJP1w+j-(`Kwm-QAauWsx8t5XG zKF9&B01__x|0NnFEz3!=Ho{@kh*-1tG2MXd*`3tvm+>h9nQWZ*(SWJJz+uxm^i>HL z7z*|OO~!sU>YP_wp96eAToO3+)enon8Vc0B8};*eTLi{1pby9(c1qDqvJ13gQo!ZZ zW3d-WUhId=I;lt$2E2eSx`&EFPp;gY=~RF^lwqK%HP9EF8uV?#v|E&nwQHipDM8^f zh1P+Q=}Eq*<3{u+sYb5V%GjaNmDQC3fz2gWNdlM+rW(eC=N;3O|Eo?0JYkjb3EC&5 zr@3OTYu&8*Sidm3(5)N9he;4@Zr&4}|Ns1t|FB?9Qo#ni!KRXQ`ST#zhlFwqQizHW z3wb>zEWo`#_`!c$Z~b;9HL|(zdJv6ya!zYTFpzoM-^7$;w{|2M6SU%Fh95M6WXKrgDM14na5Tbu=Jxl@j3*cN8FM$2f$nuh z!@U{Gml`0I6bwVE<|EG_ItsQ`eqwAx{(3jf`!lIR5FpWoC&4T~2@bC%(1O%3V#%{% zqo;Zv5G{E&_>%x|@uioT=+&7zw+>&1Rt!OrzWr}hEfR}gjuRp%2{c>~zVMTP@JFIS z8w)kbeaZ!RNR=Odqthf@L&SnGh9Y4@CxC>7wRFp^w;8E)b7P}A^;JSM$*Ned0f-ml za;)d~_1=@r5U}Z)yZ42JO27%jkvP6VMhL##b90U~p2=pP05~2 zGqBEhju>N&2+R72;7~R(r27lcF!zyTu;dl@z|__%Rs1?aw5bl_9bg7U8&TI`+xC_N zQg}y5=4NS#WIQg48^pL;glhujs8;18MO`lcTj>H8^1n5Tcd6N*AqP^c%|QwXWBE2i@bcfd^buj}JwDXDn*-Dv?x`szsdp_CGC+b9Y3cS)0Ul~+s0 zf~te_lp2;)b^Ocg8HNmnfya2~T@jkFw33s^Vmzo>>>c1Y#ItUMG}wH;rjr4X@92>m z!&VnMnLZh_Tf)v}vtEyGW}k62i?sW<3CdI1(?g8Cgslu4aQgxxt&iepFq?&iV|5%;M5O|o9 zjz~{hH+x+!y0!iFlhFzc3xrqTiF2)>$FUJ3>&j-tHCsyv^%zihM!{6QhWo7BShzQA za&m6ofQgj2`EYO?EpW(eImJJ6a~8eOd45@f@96SaNVvC zW1G0oTw)T`^K}2GTo9oM^4#$|>?f?#I6j;gds&P_>@B?Crqc$K+F+9$QkPZM3yUi7 zs`}p$O()AJ{K-O3;ptJp}h524ka9|lvIb}{m5zIK7yd2Sx?L-j_6l73Sgp;U~5n$Rqi zIW8~+?yg292x|Q=R7amc@&Bn|(4&t$YMJl<_^ZFNfMy0Mqk%i3gvqcu!5dW_4`3I+ z3oGEUM<2C0A;LlMpgpioB?2p{@}$C|(t@H0z3YcRe7Jc@XRL_2pwdOP4H}sTI(PgV z!{3(A3BHq7iAp)zJl3+R)6FtZ+4ZwTgMiyq!x)BT$?>uZ~+{7 zyDF{$!a_XgVNI|Im;k$=4PLL;<3M5|Y-LJ(UHd&uKBghZC(LGr^zb}N1}E$V2^|~m z@k@_etJ%=@`pGr^Sc9a^zcq1VM$L?t&6eZvB!Fu{mj}HETHyptr__+r1M6v~?k2OV`9M27u|6mXNQgH$>Q9jZ{24FfMkMk5BHpN;CglQdYLRTTFY$x|Ce8W*~SvL-+qU6IQHxP zUtixSFfz@+u*neoNx%Oc?YuCSBJnz@#Q>njW(@FEbRZbDy_dRH2sbgh;yUwvR~W2y zkO<<;@mb^S9{@L9I(Pu`4N+2=y^aKLJuw^h?vDaiq4h->Y88a+X$;LcRj?{}Q}MM3 zvO8LUNDZDZ{T6w%1kG=$@xXXjuk2z=sL&au)%K47!D9B5#p59as41`k{=W10dV>vf z-oq3mFMy>Pm1A7Yx>|K8(+)|R8oOZz?wb0s=x=4Jj!YSHqy}D%6|SO^-%vz3)i9Jf z9M5=Q4=Yj`H0_Y9+X4Cb`P>9LBH-@N{`}AF!T-^Z)IL~WZ?Dm5_S+1f2Z=VUROE8= z8D``cM_}MLl(t9}^e?)3zNBI)C>nhTxhRqQv5jsB zrrXJtk$<_aA862u&U33tsc&W#F~Jm7znFl{>~kg20CV<32%&;c&0N0|L&1-Kp1L9t zq_|^&nTpPw-85)h=VKo2i;XGK1l!;NoFOuE^vGdjkWeii*Lm|5Y3>Zr3qCpHfZzQN z?f%y-I1z|BSE^+Iessn#@Zmhsy+_H9WbOq`5}~Ba!!QQ-}T?FnAoh`H`I1iEwA8|}S`;x?nF0h1y z4g;eJ#v1Y2*qf?sDFR=8jvEpaC3aEHSm%gyA?d@q_k2=WWKD$$rAdEQxTfzUyakEa zNmVpt{cUCK|MJ5N1jcIs|9EalXpd_`!-#UK=yAXT5v<`D;ldBjqrX&G8NF)=F|NL8LRuE}6>%_Y|5w5l>i@Fh#y=DyZHg-tJcIR``o;{*L zj_w0&6DobH`VSs5Kwn4jowaV11byxcXmr*%%6nAG{4D@I=Jcp#dfS)wJ&6oJI@ zbxb76q(tUJVA?DNRFVj;J4qD#!ljp9-hy+(NcEoUV{jaZBJq|a5Ok$n%DJg{dUyc= z&gS!k7BQO0{haxA&mwSqpxK%hpgKkWK;+! zxAVr*hleoN~{QBFJK1Bd0KyWO~l^uLkQ!mS_GJAcMz}O zU47yz?J~S)6g18Q8YXj5IwS;@E3dlJf|6fvDm$xqO>%YCRnrxDjUW~2-$K`5xDubVg}HJU|oR& z`F9Qe90OPYM7S>1_a)IJ86@bic1a587Hy{la2CR3!tdADM_(i|uPY;g4_*iT3zE;X z_B_n{Vq8)$C`@@Ttk^YVoj;>0mt6b1v|9oHpL;%^_g||fNp6W=G9m+a@v$bgF%Q z#;NK86a@BqG1IHk{V3h+qZO}n55T$-V6Vkc#a*W)Kyn$J!4(XZ zuE)S6J{?borj&YbV9I4lCKR0V;9qV3NPUXz8#Y-yl@zUS9=Pv+|9dvG(%sch+gWZtycQAu+?E~Y#`_`yF$QfO(NUkf{dtoHUCnDUB=KPIx7gl8(){d20>ptOLoDY5nKCsrY@~-m^1jKi7)? zEgSE=`pWMO07*4V5(+~Fft>wjwHzsFrc9{8-$b&s)sNKkyD6~WtAUWppH?)%iP`pV z^xmQC59%IF%C{BO&p3?(W*qQCWdYDeCu#WOhrVM#(C41jZo(6f8(5FoI^8WWGOqY& zeDqnEVDj`uuee6g5fK}@L<8k1O-@WX@cjBhV5#)j7Bl?jgHNyDvA8stFPMMF@uM_V{8PT2NH>#y;C^(u`aofbYTe0BxxiyJbPX^vxUM) zvb2l~ph(jnzRLta{w^3a&ll*}u7SIG~e}ZOb^QDU8Yfl1N=f4`A zF;fn7_5oEMNyd}P;U)k){P4rJx#H~(4H0S|e{XGY+J!5#Lg{a4W<8r_f;sxX%xvL` zBPw%;&VE7XGwVb~mc+(pxO#m~>E=so_zGhutXY7wIVGbT(=7Ku-OYhQ2BH%gcN@#q zeY#Y-0(13QT1p#(%sXF!P`Ge;0HCV?dq6kP?vauz-KlRu9mh$0&w!Clr!W{dh!gkO`Ma<)`xbiA%&6{t49wT@I!$$1iW63Wwey}Sq zyzo2gG%yP0eFB1l?8*OHs?lH;1LDFo=)m{umDH` zUR6{*=2S2;Sf^P0UauMz+&g;)XJItKuRX{FLjpFM7IYa%Fwmx$J2t|e(a?yQSmXh_$p5qa#-Y`W4m&LOCW8=k8k?9T7IDnE68M0)l zkj@z(<}-W>*ggqEb)lXYfd8Ji3|c0FsSkEw?OCk@AO-M6##IhNv(PPjS)Zsc`Vl6R zfF<-BCx1PpO3+lq^x=cb_Gf?g=Z5x$%0xh7zRFA!)Vw3##~cY)W~=s~Z|G+P+uhjw zufP6l3)-*)exy>rY~s+Ss4fm3xFA{DMn1W3f+dxY$}G&HttXo)nLRB!Xosk$eeWN> zr>)RW%wdprA)CG_0zx<1NuvfCLT$ZmNPC+WGn>st7k$O%;ul~1B?EA=VgKZ@$Fz0- zPIIG*@0b~iR+4KN;CQ8*fO|bmYUpg9c2YNBo@SM7h|D!_Q1%4CL&h%8DjJMUnL+nBV;7x0)OE*+BCjb_>_K zRH3Xrw4N+e)lwmoq@I5NJp($DY}g26JGSdrO0~?YSZKW2jQe0#FqUHNU8my_JP&v- z(IM`VlS#^S>pQ{7kF{*(L3$k>OVsbm4f+2ZKhLb+S6cCh?OETOd>qDiT{9a@ba(pQ%B6WFcI z@Kg~hBY@JOMqOAg+=WJZ+Dv0q;<%fIikM~As@Yau^3_Rxe`p38|4!qVfe?Az&ok|T zp^Bi$s1Za>$XiC}dV?ygV)Z(u0nmpnO#4;OQakRfX5RbFNawCrISp-d5d8#RHY+oz z$<+8lD6t0#7^oG6BWmwZR5deNg5XaysUT=m&EiRLO53r}2b)YeVR)FKJw`W^VH5om zMOlN^|EC}PlXfc}S9ij{XIUe_Oj2s1Oa8McSk%OXLB9uQpH2mBT9N>& zJ$wp&s~m5HgV1?*Dq?I&&wGx0Lk_>5gLLY8Tc&HUX=e@9jB4;UeJ`##j&e}2XkCLY zhZ7DBie&26+itZM_9c2B*ethFRTW1A{T6>y=wCG=3yKeEl|cNy=DPcZTIMtmb^v%9 z-+c1K#3fmq)~g^RMkZ4qzJsb9i4${rDovA`)h-a|f0Q2|c;G=>8zfHAk~p##92{nb zLHeRpi@$y5H_a0&(DRxGX}Uo7-uqQkq&%hxpj~6VSEn_xK-U5DE4mANH8Alz%w)#ito#f#&Loi3h%9z@bE#_$^k50lK>!1BB8dq3IHhUAWaaPOG=7&60wQB1 z;Flb~*j|S*($>aS@{;J|QF1H*lv_FgBo9320AyYMx9`d)g#~a-jsP-a-b5Ghq$cVl z5A&oY5pt*0j1~>HhVBtGSh`IBj#s?fIZ3ycuDo5=<3~!Zn0oM+Y7Hp)BRc%v$XGyX z!F+7iABIxNWh9^K9&}8+p5fA<^F4VSsU8uTrnToj0^Pn9lJ$TD?jBu&)g16w#Qh+J z++fCK41@C&*IN$Gb?meZhKW$t3+ z7dim|4WSyjE1M(7`%ZeHt4LGJ5}Fr(%h$B*5Fq-wjvCm2pbvs+fc~{-PiRGkLPkw1 zALzLUQfBsAJsvoGz|Pq@@4r<}^jmMeZ9xEV7`%RF$k0gYL*ON z=*$qb!D=v@he}6fNCIgf$plWL>vMh5EP&N=_0`wdHQ`!pCy=7AQh<$gjTjX$zvp$C zO+x{Mt4<^-C(hwA33?T80UI0OtH3e{#`8`&^rVpUN(Z?Qp#IPK^|2LJUN)o`m zd*sJI0Chl$zp~)Z2n`&;9m(a=!IUduwNk?5{l>WV`39H_Mr<5ET?L#U8MTp&a;|hj zeC$;ZSY0m2T4!F5mIUWT_k#cp8{$iP?WnpJIsWDEWA1*+zbBD`^+5H{?4G2Dd%Pjq zWouR{g^cu)9fCIr1m_Gx=ddO?JNfLt$5WTiVRStPan=hbiT(om6`jdczXR4K|It1e z@L=3a4n#~-+f9>cLAF|fyhMwCDQaFQz;qd2E>r@18@R(b=B*EDm(f2Q8G$cmJNW)z z=p4#m6d(&@Qteul`Z%4MZ__WrW^M;8Rmt5jh>x;pJ|cyHLze`Mv~a@Ee4_6tyhsN? zCPWz)cajzd|D1ZVYT_Lc7{|jyg+n$@CC_I0sEYeA2gRba^0 zBt=2JGY)t{F$VB;*~m@iR|EQ*TXt5^A|-&2>WyP22|)OSjqpdh*`n|V$coLFB*5fQ z>0qp_wd0N<1*u6b=eYXVoQ;pI=SYwR%RsyLm)a3QH{dtF`Hf;FPTMiutYZOMusFQA z=X{HVAJLhN369TlHSyXyXVt&YBsu15uf3+ZfBXLCeNAY<2@r@77J6Bo={8aqgbkL~ zFM*H5;pox0>2aA{|G6*ij0Dg2XqGG6|kFvl2R6MHIIZ zth(-yEaqQHbU*;0tdEa%v;!iXpunt~YtAc?@NnHhzLNmJ<2Thw1NFsDk$J`^H%Hbn z0fLr4!auMBSeqnTRQ-3{b*Jh18wWjs(C94u@)yzdk*>oHTG2nK)jO(v%$RBElXSxA z=e6->z%3Gn0g8f1Y?C;${!pR(nz{rM{OMNMJy~5HG2^N1h{+%aBco?^jgz?WOflZ@ zgtEoFO!&G=mhi47!5-^4X*Y%INOHsT>Y_MPRe%1hvhI22*em##5eTmrfuQ38V7WmN z&m6#B^eFb}y@W1H_sbPbOD%(l^@?qL5(nhSY2^dDB|%2;YbfX23;>Ae8!$Ei_{ZM? zShTU;h=N{i|L7EHWg%8?ssb*Zg`{FOG$rXfCh^=Xkf85N8&CMs6elJl@`kOG?x^8I z7~{T=Cjo=tKal3n_MrIY3t0e#KVOSO3puz?s)<4YqJtCwsbR|=|=d~uYJ{i{`Y_X_XfB_5azG`>Jtr!-!;10W(Hmg z6=RN=2+Ufb9rIX%Db?POb+iC~8pJMb*8A?i&mQ)zt+@I7ym!np)He-+8bG-R^`Ql= zj~W{rG9v*tdejK<0ob?ifNe0YZQu71sB!AMZUI37Iajn1U<=R={_Stn>Fr-1(*W=@ zYtN(n1{;C4rJ>L@h)~7uWT3TKMX|8OC?<;4EK%WjU&b!bry_Oj65H_Mo^u~SwSSVh z(#(R{_&6sLgD)w-kV^HkD=stSJ705ca04`-_3fRuH9AkCI_9dEEPXqEz6>je1#;7_ zYBWrQ%6B?@ecI^E5*ypRslzSv{aalMR04QA0Oejus??c2IXK?DZ(i$D^cAq%tBSnt83-wp7c?>VJq3`#snjRAVHa1m9N(wtc6=J z^EIprfP3603}C#q&#^Msr_*s#6R#`uU+ynm4(mYe#QV>6-AuC31|&-^awJNC`jC1? z%@1cjTE+lcLG7b|agkPhJS!l_xqk#8*Z>4?(C7pc#C>qkH1Qt|G=MhY_(SF7gT#+S zlK}dL&iugwJgqtaeBdX5p2SretPz>w+H}kmSqLcRn(keVI2jUJl0EJnSP(jOc_lu0 zRw~cAYY323a*gPO;9)@GN|MW3b1|zjNtTKjk$Yp1w6Q<12F-m_6BE|lnWV`-2LAJr z9&3ST=?$ed@->||_8%Kl7*J9MYNRdz#8@K?e0XV+NuEMfl#idbug&&~|iEkaWKa&TpytL#@16e#W zX%nMRV=bj0Nk@WOZDg_)(&ubtvI7MdGG;Uq!cYfD22G5@^q^;ABqRO&fgqBMh&sxc z1m~12#pJauvcBl3g)j(aH*7Zdsdo8!okRca-~O$&MgO-t^l=rKX<{7IjYf(GO`6tk zo7Z7u=I3lSxS_F8fR2V!O+aHqdg!~~F%Ggjqd=&8CKMPO7v)Zxbxpj8Qdw z?%C(8b$(YeZGqZ_M!vh+*%FwArqikEn}tN7oOlcbnXPmV7zo`6y-WGdAj3h3|5g(Y z=J*5z^b_cKFvGNJ!?^N0?dCJZA~x5$Fv~2MV@unmZ<&wMfq^D?t&Tq2Ue#LH>Y~VX zME3Um4^Hc*{JI5L+N!*U@uQPODv6eB{6|N@wN_aV^A{T%tgL1w4+eS?eB7mKk7)ceW^gcFt1B-u&i9wT8r(eL zOQ`-yV$j66L$L}ZGv|CxUm35A$?e9L{a#}>0B|J%#JQ5SYuC1z?@x^Y))L?s7sVIM zJm`=m!S16lUP;DJPc#U|Z|YhF+D59eq|9`+fZJhjw2t)^6FF z%l~pNS-|jp)inng$MD?naln)%4_Vi8JE;P8qkV0T4V_zZ?nvGPRF~VL2z2 zkUurP4$Dj7kB$hcBY;sEqmb*b%nc>o9|g4hy#)Bn9fJExN~LOJ2vW(Gym#Qz>}>~n z7E%H*)j8-Blx6Ei^_%|LV%+RC&17NV1ECm zRv2*o!5o0l=4acf4QdjlvzafnY!(~ZjkDDkSdFcA+KjY=0VGoK{HKD-;4c##Xc4oa zUUl_V)-`}WNY{X5i0ejhM8s%kE=6 zz^AffbECrHd$dDauYq*E08deQ8TTILy~L|GCB{V0%n-UQNKm9xw)FL8u7Sq6j&pbd z7a&l_Bv4&KXXLtYPXU@)wU(R+!G|ygB7vj&+q*w9Ina*)ozB0`K(TeRcdvCZ@C$&G zkIe7uuf5v5sZ~5cG$f6A{hMyOQSZYE)1M$v!wzt@MdsBnX=P0U_p)}Q2=XMT+%MYt zbO{(c(5ZU*>8Fet!&rdo`+h~=LjweXkLb{&kN&v%#p6%d`I7{K_UHZR;%ubEu^CbN z>u(D2S>YTBN`Ul!tGE*qClVN_b)eM=)^t~_QV)xRe>Cx0vFFq)cDgQzRFODekm!1) z(&dyF!Af%OBhh+AC9k3BLL-cwn5{{Sl0R!Mp-mCMYXNNK9+)cN*7g7&5efwBe7+Tv zO6`7eZ+RAtm5`JJ`=B{VPM7hxVQp|Nc+G={4z*pos%Jr1Os5MI#a1iq6DmzynFa=y zLOo$QK^S+|9a?se&hIs0(5Dp{Gfo1--~7$r7;xx&-}|2Z4%VhPu<`$OU|?6VyBQI`hr1V*^s!?X z8TIa`|Nc{}ps2<^^N%We-@V>H(1)I& zc%vz&UD$cb_sn$)UPZ4PAl7p9c@v;8yx?hLLla{z3{wE$-t*62)Vz7~n|e-X3_pNI z!OVT_5wlqdoiK}Z2>HGaRw$+`67dB9-8MG5ze%lDDtdw_0I_@y&xTFVAQ@oBZX!y0 zt^aTD$yGakIGbKqNT>&Vr zPio~47yT}!(mtpOGvZn;554e}sN!?7XT{ee3EHbKzhdtX!+?$iT^hO?F6lfrqpwj4 zNxs3|0Q$p;5Wp#HhW&a!axaOg4uDfO^(fmUitEC?U`%%T6_?xJZ*6T(vIy)MkeSgJ z9pLH30oT=&gQgjC4V+n5jjj?2FZYTB08EGD3QEP!PIDl3E>7!lgQekpS(kfT&4EJi zBrJioT8RFa<#m7-P`sBG0<6%-r;UK>zZ)_@gw(pyg(pFV+7C5MfsAA!(xVX06E|TN z+-Ee#Rv9VSTj^ojRCmCL46~koyP`Bw5UKDx2CM<;+53jdAVPYfL#l?(GC^|~<93$U zI8>}+n}Kw&OwOgGL_(ICy0zlw29Ei+T-}~M_*v9z%?|={;51KRWK1? zFS-=in=*9FKe(lnkb4? zSj?5_m@rqSs-+5LeoJBkik-k@G^W%Ma0%U}=@8DjPeR^5l}fwVB~_`!XNdve-)k#p z4bF@~(DLVe;e9*S!Lb)+E1X1z=5ra}ZJarn-p^h?brGD2V0!&YyPx-Fyx?pJHgMKf z6f=td?;Qk5pj1W$Q6e}pRY+joek)#AMlcA^?4;TDwMyl?R2cyPL!DN+X2LEjR!uXG z051Ur@Hrh309vC8UV<7* zXMkkpcD?V61qlAMqpAE`5L&IsD8A><4jmEJ`wO~WUr`LhMVchjO&~})fGsgitI77j zB{c~wkxY}=y{P8{p!BZ0?zZ(96S18Ul)F@M1Z!(EtGrD=K+hA}Di|M(%YQ8Ru$aYc# z|2Mp>6k|#oUUmgElv;JSveHWIvY~$akN?&9 zW^c-O6%THIaOcMJ-fXjRTJmge{0zrGTIgqz!W#&CWKNypwzIR{THlry(#{YtFZuOo z0ZcoQxghOHz*(pqvibkyy?@j8>gncPZLvc0Lr?vX&BpiGtGPXI9BVWMP;izO+T1oS zPdqpW+P}A}0Mgr9)p&cE_B2%v)e^H;HoQxi63$-hovvy=J>+ zW3NG*mu?4@Dr*blC(!RuPfsNl2VMLRE`Sn!0$sjen%tj9@`hD|Yv8lw=uhaoI}wB2pd@ez!L zNLmP*1Y5d9B;Ity@FQpVhK$Q{oc{tZ zE{kl2|I31Z9fUaDMF`XCgiY18venFydjFtku=FW}c^5*YMwCsUnob$I@f2=#k~Yza zWQP(ASO>^SA+g-0MTJ32C`I>2mqKVN3T|UFq3N4D$ycqlv!tKdPCd3>NwOIcsAz#= zDN84S`8^LY^HXSkR8O8^*E#`BR;l{!quQZD{?kAGGfg6nt9si1HF2LGC_@0Zvu~@- z>781w9MRv|+?Dp9(81Dk{!2OtA!X^=O|z~AZS7eU1>(NZ>LwVY_($6syaLyaYXoD2mN)?)3Ks#u zIt2*N;G{U)GMowL0L2h+9%BLmKZimW=8ik>uz=1Se@#0RbYgIm!`}DbO}qacyN?mr zdACXA@;=Y$>pYTzKOgt$xQXr<;6)Oc4w*k2eR4~>Z>+5M*yZJTLH(0h1Biq22Y#Y+ z<_ptIvz~jP*fH@ulKB4WS5I5whixz&xR|8$MY5*a|K6G>F$4GrIMFDCx`ydo&^gJ+ z2({6N<7-G7vEhaW3ghEyrT*pq)DmhFHrDQ)Y{){6Np+y?-ioaf7A^RzC9o(9%$P2Y}!2F@4WqvZNzN+ zY+z*Tn_F91t1$~)>AfOCe#f$WTG6!U%~M{h4CbGRyqGGSRz=LdUsNx2DoNUz2*W@N z3Q43S>i+Na75<8`My$R!Q|Dy0Ijl51HYEZZt!WHONNBJHep5O8RGV*U6-9Lm#f$4| zGkg>FIchDm*l}ZXlHj%zXbscBY{cY!3 zHbPg;IuG7$h!VZgI0&%eIh*5tY5|{5DpX@LXmH?XW=X7{7{jnxLlfg3oj7rgX(X`e zcg&F$yHBV>LeYFfnJXj@uPH##_}1TSh2g0`vmwE=aaI!`@K8=<6@I2sQmrUcd#I+|dt z#2ToJSWQ%SndLUmDA$wG7yqIufw0jmfk&qh`+4i1-{U#Li-8f$8=A}l#H91b$i<{} zH9=b`9RHf43mAcRn{k};cC>1~`Q|Shz>(L7o;Oy=w0v>>5BEPae$s$Wr;iR^?OeLR z5Pt@^n1TfnAi$)IbN21OAWN>oQo%p(7I)ybu(-NjC>bE5C~aBPHkM_S_s7fksz**3 zYJo_#cRYajH9b4hP$R&Fe)8nOywVxyyvO=LNGZn$hEU09fYL}!d?O5Ljgn^}*?2up zSc_UY)Wm+W?J%^xQ>maCYa$Xd;s!V&HX%a=he@A@Ng;~SOtfhYJ0!Vo@!$~bd0-#@ z=YO_}`k(b*)Z5m-y>7xSQ3`a{o5c-ha-ntRb082FtW-X1)KIf%+tO}iuKo>mUt?Cw zrq6~WGs#-j!+b10^r2_b3($x};!c%jr7EW*U_nBIGoJ0dKITFw{W)#WTb6B=RGHz% zL}b>UC$msMX_A0lxXZyrASq(@$@_sY=ez(Z(xwK$#Ae1E`*OVov{h+svca0iLEah- zKBk!!6rmwlsMV(qohNSyGEDeo8i+J(T~|~arS1*j7z5vE`Ikxz`XcxD0~Ldz-H*W0 z#~*!c*Asv!%#Pdjx`CVEGghvh4?=9^8t=%k0_Y$xenH9aIR*F;U@!;b+H=nlyx}## zLC{8qx=133uoLt_K2XKOWfFiV$tj~AJR1I8)gXWOyJxMtK=3DFWj#6Q%`1Ny+tk}F zGq?qs9<#1fJ#NnVrI%u-15o01*I#Grlq86ASV^KlqveMTfp%2?03S)x(W|)15~mZE zYy!4)%GZp?`2^NV4ECxQgmu~UQvsI@uiY13OhSK#yB1BS2=K<2+ zX$$6xUNt~q{x3kWEA)Y0)6JC)|7Sn@sm<;md+br;wnM+-wXeGBN)t+n;JvLj%ddfd zr0^bRBO|ylT0phzKVSM!3;v+@@p?z_g^Gmnz%5#xutBlG&`~hZUJ{{kz@F)nhmH>{ zx%PdMa%J`os139{2^8K6mudBM>z6;XE&%gP5(5$e#smZ$t~bF3Q5ryvY?8E^xvob{ z^ZZ~-sq`@f?eka>r}06W&6i4lLq72k6jagl&05B@7I$I#1$vIg%9uq^pap&A@ET~* zl^|fS@iQ`F<7Sp`B1Rg3x2+2lL5fFpG;+;}lUn6pY0iv?bdzTM!@rjJC6Jdn=_Na9 z7!mKh_qGPmmn^U#q5O$C1Fl-&$G_o9)w<`N(_x|24DIyA7hW{=NHqI7CXy{!0JQw+ zWRZArKS*%V_NRlxT1WQxo_oGx)$gkD;Wsl>&hQzO2~84Dg6Rr9rHX|n!lhtBM38J< zlQ2z)@(i?GQa6gk1zgGL)9)LV&_uj6x#pZUQ!>1$@-NjMxt|12t^@ZUU?jmeZ`=3y z@Rd;{_XO_#3nt0@ky$k}db7^J_Ez?+)(y-!H`pKM<)FV0Zs7Q3F#*`Ajmo6&UjP7- z_vPAhO~B&Nv0S%qNO3(!xcAe5z#;2Orhv3l4tp{RVlPm7Q)mQPP{Egmb`^yF6oySg zDU)At?eoAGS{;9p0aswK`Q%JQk!GN@5JKOv$;X*tZe~E)4Vt?CQk7u}Q;F;Zr&braJnCR*j&f3BbQl3gKx53i1hv5PXA+=$Q6DzW4L!R5#Bpbj3Sgp{6I~(2M8t$jrYp&86Q`5L z-*e0_D_EEJz=oKlmNh{2zdGh0Q0WA?cL3KG(;wFJ<(ix``Z##-LbFKDH3VvfivNsz zM)0P6AK4q%+FHgCOtRU%6iE%w6fBc-SUKy+hWp5Ck`M!CB&nvVFTsqhcYq~}vFpQH z^f-3rUNP(jQ*f7oZ6$_vQL@L!MREqZ{vub@)g;QQ_ z{^U>p)C6&yCg=d2F@&%G%zjrN_N%lF`al2IUt1MNwf6K=PdESg|9@XA_NUBf1uz(i zfZLL~jPUS1qy?NcS#Y+5L;n^*6e{eGK6=nb0$>mb$d790;b)rIP?0)l@5(E#GT|DA zO*@-0_{m&aT3nFMW>X3+1W$S;wZ#c=?heTNGykUr&87`P`x70jysvFsHZH1VDmpq9 z*1|Q~;w*BRwWDy%SOphaGKQMoA&iRlA_KhFzBZnI647x6fmiaO$!sfAx;<~LQRCMp zqLcI%m<30b!cA+DGgswbgk7kk}<{KYIh9~Zo%!3;eD zWVHbl0!*|-N9Jv^mJ5kB6WAXI$a2n%ReqwL72s!hO@hCT@iOJQP&uZmCjrWXde5KL zP6^kIPRdO;-D3B0R?PTo8V8m{@O)}_3ZW*(9K0S$1jhwF#-7_&5UZ~o=k(XrR3z~C zjEqRgjKQNx@Jd4ek0mue>LBr?OUZhp8x&a*QgN{?(O%Nh%{)l7xk9K^*HaN(7Flc8 z0BB*9t3QSVsqygBlU}KNhZSyL6IVv<`}Dd?+C#!yZ(I{%;XkH zebxeWo=@>(2N4l)2%tPvRLMxD!Vvi34Rc45;IJ|FjSZG91SRsT~**`B#{* z3A0l+JX*@=2%r}aQ0yA5e4^`FY%8NYXf(w{aimq+&bu>7h$J*VH(!TM60BdlJ-mIFQige$bY;0GibPsG?#5kmz?S z5qWQ&gOqJYr@oz5)h}8y#{jyOG!yLFrjfuFpiF3L>2Il~_&M)KkU#{9Di38z*1(Y@ z5QGN9!3z#p&^IgN?PO)Vy<;>?8w+R`fc0|rY!rQYNALG5TJ19qLVXcYAFdZvIo9AQ zWnR$D0o=v+`C9;I0PDDyru_H&Xk5eJ!2URO>P8Fx7itnkC*~RL3VFWm zpuEE^z%{g;o+TvJvRSw%B_x`>7x#z6@`f94P?g6+hGTHSD3N6_#L`ykAvxrc=y0AS zI3$!j8-C1fm0d{!hN3$W`H$EYCuF$ESEzwf+t?2 zu@o{vA`=ZW8eqlRQ2LZ8M>H4rIuFlIG?dICP>SCx)D{Qaz%XvuQ=>e{=tDksqeB&B zC=2J0(^i)<3Jmll0C5q%BFMA((C$Yj)o0UGP{^bb!0-S-$DAPGj&W1Rm;PZKW`CUH?i(Y+^$)Yi0#2!YpmiulKI@K^*W7 z95iIQS7iiT08LiWX!T73z}L*I!4#Y~wDV_Hu~U_E4L4jpu(-}H13b%=1GiHWV?G9z zxi!~_ir)MK_U7}J_*BW zlGOb`ldI>o;(z?-KQ{p<&L4e&Z>fpZ?OOTMfpE#(Sz0$TDn?r0Vm+V|h`=5&)}f09 zI)9!lYFAr(>~VSW+B3@EnLfXc6VU0PvgZ8gUU0u$#j?cVjO{HgUq6Zdz|7#9vkt*a zoHvQ-?NJJ0UOkg^v;G)MFwUZj!ZTz^YDed;0@d=OfMX#E=6WzLf=*i-`{mb;Z%3!j zGnP2BwlCHZ4ir$=UMR_Tg(zoR2!=)mlrN3@QX|5 z7lF&0LVKj?)yI;n>bin&D5UKqpV?PZ<(`Ud=vow|Rk2hwinH!p4rW}@qgc%{rQXU2 z88aaCIf*h*^%*?Xj__|Ca1x+eY9dQvkgeqHvZXlv6+iZ|;2jop3MMQoC1bUTEKU|Y z$7yHWNk5*JrK{o5j%JSglb<|h_P?N=((9bZ%Nv_H}0SeET>CHOG6M%dtoqH0a@9$k8 zumhYIx*HcO^|)yhfDvu`QtfXS)%`%@uc(q+>Y&JoOz?;#aA+#}1NA^3WUBKZQ?P=< zs;p)N6pATD=+T9{#H2P-&XIt5tGyEkXvzH{51w6zB4qZ$uJzz4&+0N}2{N=ply*Q{ zx+?TnLJ!+mxp-k&Hl{3dpb24AvNXd)w@G9<5Q^-wg;dK#qlt`I(BT)T5>Q7YC^8cG z@sIz-j8r~K2ykYdv=FJ99laapwDZBG%jtJfB6MyWeQf*RV01m~S-}S|b^t`jW)7;J zL<6waewF%k30gNU@_A`qnXQCwHh+#yA#ExbYmlI`aQEG!!Qn#-EH9+-09v80*k8Ys*ca&eGg_jvK$i)A|J`@rs|m(&yJpsf${Zr= zNcUzy5tX!t_onI`HApZBBsLrm$b4Rxq-R#V{xafTUip(4qHGDM5fhs6-Eej4{oqyyWFU< zHT!SLxc{z@iI)dS1iz0S#1-m2z}SZ1?(_eCas+-sJ3^XZeya3Ez_oPCV3mBylmidh znqyCByHmSG@GWTVON}~^>D&d8MS3;M%LI>oxtrj>M2EOfew3^yT>8z(D5Yq`DN-^6 z-AwkpmH-zzMOlr$Be@b_vf~?}<>oIOT)eC{&FC8**yh{`9Lu)1>wf*g2d!hk`=HyuG2cw{ z`R$an&U$vYz4vQ_Xh?v&U@h(&byj=%<(HYE5dbkjkI?qmfYFA3>dB`pz>s_Z97J-K zV+15g%iPrxO(NF-g!9b(uRqr5H3>R@%#@S%{njlYM}$F2#zT8R&NT^77=o)xc>uPuAbFV=LF&`6n(h&fiqT94;#iI8b4g3TzR}JL- zf2YDbfIR6Guog%z?lDuKn@xPkup)Y&X1T!4MDMPg!darEiP|0=laOpYs?L|R&Pgz6 z&xZs~8>7hf-Q+^GL+0S8m-U>V)@~b}5ikpY+yG|gAh_`=-EDdaxKZ>6+9qQPQ1{0# ziENqUmt5L1GNOEOy`}4T2d*=}&$B^t&N`s=j|RW3yK^$xzV{C^@X_Sq1GTzl9LDjq z*1y#eW?biyVWc2NU1uLD0FfWi(K~S9f>yLhG9GuWN|dqDtTdCLyeZ?G{@xxYe}4>A zSO4BHP=-;%pL)hnwchRd42K?qUb?oow9-F`TpvQsp-wUi-(b_t^=CpQA(S2kuqDEW zp3>p!B8`M*Ia0@>1TfScj0XdOWh9z*WeZS|OX>naadkwNPV<&XD#F4guc#_6Q>8qD3r5cp%-8 zogSseoUeAIaFxqZyP?PLZ2>@epikNfK?3L6xo3Vd^%}LGYkM0NpSw z!WnabJE!*qB)to2#Xny8msOKL4_@(Nbj0-pyMQe<2`c=2l3%V7Nsy~Bj^TXr z*LBKoMBBfi~<5k)(A!J`1P-^qK`i!&2%7)B%K3NPm%Kts^HC z1w!BQV9Y6x2YjAdr)-`CdQjKQ?H6) zJlOm`n>?EoEnVnaBpV>DfB5hZH30v@HXiT^x9A2*B0vSQlUdSs>1^oJW(q{MN%HEa z0oe4L;2toQx#ym{Exc_Mv?JuGVd>F~azI}jn4%Cz{P|XNJ7f*OU|_aR zOCJyz!Qev;#L;5{>)>ON$zd3Z%Tq#k(3k)u7h4-!hGED9Ann{&w5mi@=Jz^RkMrHV zI@`b2ETykkH9asA`}RhYD)`^DoSDIc-=ONJl0|v)PVEw*6lk)x)}h%-=<$^y=(aYC zFCH{cR!JgAZWvRMczjJOf5uiO#hdu^O%oOZeQRI4T{M!q2xf8Z@7HGuj6)p9b&1O& z!5>fZ=#KEWcp=bDLHQ3B#7%lV=_sr_=YJMPs^MS~$M!p#c%VRtdnm~<*M;QgI%va| zJZ}`b|9O9xYlv<|W8)#He*i&w-cibABu7Wj1-XVk8KxU=&-c1Mr!YoH0(e$o9_&e4 z3AL_6O`R`HsQSs0)tfZqzAREFl1`3Z`@QP1fGtDk9a-!zGZO2nU`5FSe^b@*&rvcX z*tC%A_7uREUhp3>?4$rs;Ty18-+EQgKGX@~?4Ds(pXYp#9Hk^#U&Bm`_m zD7z7CXyspd<<-^_M093usPnC8D3bihT~4^SQ6_%8SJ`ZuRC)xfNOL=CZB8oC_cSQ6 znZEVrTgF{R?;b8REm5=v0L~ehPpc9V1M>h-G`;cTj~uoNk7SDK_rO6{!iy9`Nos#P zY4$$%F~D3pj6oy)m0W4)vH8H^!upM0++WSW}2>{6-J0trIf573)ccjIF(9vOjPC!9InTj8D zI`;ta9}6(b-oTsQbrHCSyg$;*&nu$?CI_=*)(mpLcWL6ryqtT-_rOIEI9gnA2vtPC z462;&cMOoI$-@h3D~uB*-3owj09UOOVF>!2w7z61woW&S*|FE)_vkE;%z%F&!G*N| z8-wmz$mo7GKw(aw+ey!nd2Q&wjI{U!XK|f^Ah|C*$F9|%kEo=urrt@YMq()&%JN=i`O#lzrGI1$d;}Kb^sQPN9!?jD7oDmWlS+bgN!x-q!>Gg(Gs%j<}Kw2+bqd8sA?62 zA?aZbKU!udkujL zNbbpBK54)x=CD+VAAS6xwJ=E#T;|rPX#)YHRGm6)3KTW(!|$_k-Fav9JcsN3E8T?Q z@KZ%RrT-6_pQM0I^Qx<_QlQ@@HcmjRyc3;~Og=YftIT>8LFY9vZz~TrQUVHrg({dp zNcGFP(prTNjU_P~;|EC@?LeaYk;JgU9n0*3`IqDXw0M(z&1X$NFz0TIUZFLeb#}c{ zkX-CqGIaO6o8zaEfQ?Gr`ggw5YAFzWsB)>W&n7V-+P>c6Kbz*z1Vs`Xs{G3>=pVP1 zFjla86UyGfpj#;s1F-03Ox#}*rxy)?`I61ld4GZvt^PaixXbkExmNSUbN~Q{5+TD8>5jl6Ko;5O@Uuce za6l23u1MguSmz|&9M{da+-kZRUEp1=^MM!^*bu%)XNWZf1zF*xMbPbnxUpCCcg~*O zhzV%w5(19I`q_0hdWLjMTeQn4O?y#`TP7m*nA2aMoFvTXn%8yw?dvo@^sgty31v!ApiJ9xNdesn^}p3T&B;OjgPT!F*?NwF1?o}8 z0DGg{`+OuHGJo|^O|S9AI40sV(uR}?K7w!Rp#~l)^#zt?2rSUP&fEJ0frlP?$l9R< z=3hMV3!^VFmt|uHh;`d-w_0TguirKQ+qPz9e3DnJUcHlWoYJc6m{J0vFc}reT9U6> zBJi{-3Zf>)=m4PEwI{DNp7TN7WT>no*E!AN6A;Vw@0)`18k+9>+7>BoRXu)!ITg{B z8u$PPfE9p-yr<1Vqyv&?JUgJM(62kD19bkLCA3&RaV- zImUUn=csjIF0+mX2^NVB#|0h)Fdl7KMjimesN_7^VsnlgbfoifkRkpW0ULU^xM zhICj+;Ajbx@VN>i*M9HXKD&2-3!hR@C{-w(7b;53g>KiiaJ7!4NCY?rM}mhEKhsPK zzqhu#R%#+{z>etqyr_;sS zM#ReT5uU{KxZ8v3vP&$u1{^zhh$o(S+yW+2zsM33bk|$)r!XQ8)~WuPk|S!0uSO;Y z0#4vCzI^MKEm@1taTu)yV*sC-4#fHO&n#)Cdq&rT4i5>Q`+w9nI6t>2@w*;nwY5uh zfj%>rsDtDY?W}o%GKIZZh{Vt7cA-nAb_cJoBnDvuYFb0%he|Ogwf%m0wWMlJBojj6 zxgk{o1g4lak6L;mw_yxzneMe|Yzw%Aq;&Jw(5eCEm4E!m>~%wHES$wDS0ae(Q}>hA zW+_A6WoaCP{|i}1B#b3@elnf}^n-u1>)^i(ZbFttGRmlWaWf8T{9>@hmhYab2p&o{ z3Fdf6>|T)+{O}S0$b<_Zb4}FV5aco8kz-%74W8=IRsKq#V7~UEGuXVsJc8myo z1Lru6_PBRX)DD=f^pc|7WF=FmZIS%Zz$Yr*q2s@0{_1g-&3)U^Kn?z3eIwEPpnBZqyx#c>Gt4ge&kD-JL-@MRiRIG}Bol;uAA^fTjnbN+Njp!7xl_otRj z0UEtlTiv)$!qw-!qSyP5Y4cMtlAtg*{*~(egY*Yn$n5t<4QR~zPb&AGB!oa^R4}W8 zXGtY;3DLF?QRa*%;yu`7=SJXU-Lk&Uo;#yvPq$g?pALgl{p$FZ@p*D?$ICs2DhYTI z-wnW}cWGxK3Sh0q@fDo{D)o`zpI82#(DHM-Hn3<=C!{+=_0PXe0ucJXJ%)YoRxF7f zT^FdbB=oEesF!s7SSv0eUb_rYEjAcPZ*yzI5*PoQ>sKUPaN8hySn~?x~Y$|l~Y1dXH)eY@aHf=T# zYjIA#XGr2CD=koP=lpp)5}Ot4CW75AA}myv0QA_XXfLBOhn@fxThvg$E}0G;O%>d{ z6%{HwC6WUNEfejlky#~8^^=kym(HcFe+eQ-x;gP)79?@eGX=jht;*jgpMD%W1+>|d z7XSIAcn8SNkEsM6T)UN7s}AVN1Nepm7UVCqKz=}jJ(WB&Sz5AGc5L)) z&afotMz|hdx$D-ujd#x)gN0#U7N)r`Q4z8bTA2dh^`8k4@hh#< z90?^D1HS&IO6Jm~;C#&yGX>?b*OLiwo^$gfHSbwXR$sK^#!~@346Y;Vpw)cHgssM& z?fi8~P5?&&rsNnIi}1-h;Mik-D-7vpB7Gnj$+xa=MnBvKI!ILiS12aN4ZFs?Msms& z>+SQ;Keg)#O%%oixEgRWYl-$5Y89$PhfeMb!Qaia=nMg7WUZRHl*}{xY8ln>m%=(W zZ>vgmq#HVYmnjgSEE(vRF!qrN%t0@5RXYlcSuHi6-`uE425SBN&U*hXLgCGmRqrjs zIrJ=wJF~ygPhnb}-nLy3^+6ap2YFD% zFOi*w3Lr^F&?~abHvY2Nr&N-ZFp>%9=Vj>oohUQQ2E>C+1xbLRZOpvZKwdAuVsm>C z$;hR$*+B!kX_6OSPiB>8+dXeW>2p za9-A`(-hF2e$qA`1XTz+j3oG^ngLzTn7{yIK%Bq54Wb?IPOB|G3t`Y6=v)JxmN#o% z`b?KUNg&h4rY&uhM-7Y{j0|)`qb-k{A#Hb4_&XoJzn;_v`8=PL0jY-PEsG$51f_7u z3eBL4LZU;^gq}zM;5u7pK{rtXyajmxoqWAF27JC=U0H1d5D%gqPjd3(TxQ8O*xU3} zi63jg|4@TG*Zkf0-f2Eivm=6j4C*th+7W$WO)xg1BBkPI^s$!cZL9&zfxraNEuj4l zP>^fQXb08@I{W4op~0WEXW~J${b!-;P>J-+cYIT+t$pqb*NM&*iR>%3US72TO|nQk z9-t{}ntN6{-PO8&k}(K!6-*al0y}_)#;;V;n6-BM?a}Gb5%sJ)7yDUeob8wb?&F)O zTSBr)$AahVpeA=)$>q_kmqTU4OdmToMu=itAHM&AC0~pwOgH1EDEia-V^lf+oy?h2 zM;*9H@=MZ=!ltdA5CyH=hFc1`_s)3eeS4nIocYXHH*^}&k#Oa~P_k!g)DnYM4mnJ>%|*uj6ngU6Qb-bqP#skziec!) z0Hrs^jK7ogT+_v0m6|bwgt4qCOv&?C`2IyFB`cqrGTj-`GZq=YX;Qai9}BoOU+{%q z)Vm1uqrrayW!lXj>&I+ay8Ddl21E72W<;gHoE*E~U;p~o#uR|#%;p5IAMJevc=(q< z;8MA>*=nmmCq;YPj>YLvC>CyPv}k6M0RUud)&x;FKt=v_~h0 zAV)%E!8ld@(Pp;~WGXx6V{3mcB}V7djF~PHi44gh!b4YROCJ6?Y={WToGteEsNe(4 z+F8$=zzmz^Sv^O_GE~(#6B1zk*;zLDdpB||xdYNYa=ByzB@)71e>V>@lb%|uy9kOT zupsksY2-M$S0rGJCs6F;p5+NZzr)z6{_!j@26{u25|Rj#;}iNiHjYSYmep!E1%g$@ ze{3e#z}Iy>@Bx69XNR?A-5B$72+Mh~g)!jva4(#!{AJv0CFE`Mu$)!!(xZc>kk6Z9 z3XCKMQjs(knUFom+wK#71DPIhsCpK3I=>mXU8?zouj|=B3b-wqC5(z7R@@O1QZ*Eg zJA)p()KQE%KO!sy?e)wrYME46I0LC?Q(wHguY*J;yq-jsloOT;c_DJoSCV2J)i&IE zDsQS0z(j0Pn7RPs<9oL$>KPCrn+yTkBxMy?#~gc}3`k}n4Aq@8(J2$HX%*`Dyh1^1 zhiwGFh6sxJ?RVa?|A)U$TN9HbL~1U({4(nfKsf{5%qDY0jc*QVl@YA%?Rxy%hE~tV z22E&c0-9Z>9EOU7)+%>t-(+hX&mpuDy`0OQq>z?ZRGk@CR_=r%Z_S?7S!S5jZuaw# za1UZVu(?Q2(%8la7FhjSu157m4GNL>|DjR#-qmYL&}78J*Ho_tkVL3SDO%|JQ$Yiq zgc(8Ax02KY3FNWeea5O*DowgW&`4>)BL+oqWL&_ypnE`fg7fB@wXNUDF`JB{@ZDLC zu@%Y0FMjca&CR(7jBmh?@SdHdMA*IDc1ns(;&)}m5>xXk(Dx#EeML>L=!P+Vk+DKT zhAQm7kxDv`1BVV+>;G}Z++dV+z*H`8vf#fjc@xxx+(Ka{<6=@I)nHhlCL-H=jgIWT ztO}hZZzW!)cHC_Opyy5^{+d?*bh-d}B5VYZmr;^G1LgMZ2zh@Ez=Q8Ac+kYc^qd$` zxf5v5KczNDkV*+B%YmIITlQsJUWTfEBF$tqIte6P>U+)fIHCB(NdaORXx1-u~99&MWbDG?6Rrwa1!$ujgi&EAsh0$`QW(c79or^zA_VuhsirrgGgX ze3?E#Xe(NhrkPcpA$0|IY=b_d!I_|CpIYfdhoiy=$^EGY9B6p1tq+5N3Xv+G**>j) zY+4!5@L^!ibdApI7{siQl`0B_l4}pZkJp3kL8VV;h6I2y5$nK4BInPyfqRzYHI@wi zJ}Y-hDhbOQW(xJDu8EiJ=T}~NLTz_%wc~dMzLkP9x2!9tzZZ!d#||FiH`)#3ed&nM z`e&`NHgk53j3hO=`pP^d7VelxmkS>aItzi+E<2Eb27P*+}cwFQ4i_4nk$bGDZy!I4q&xwid3)b+;N z;dvxMcVVE8u)aGm=6`{P?5;bqUnux5(LGp_={O$zmx+IQ_j90>nde@k!GA<|rW<~U zLke7;KhY0YJEcIED6EXtGlijSb%tEkN-FL8O71Wy<3jERSTPAHRXG+E%*~WHfixBC z%G|GURJM0_NYiCNep4C#n0Bya$N>*7E+fhZls7oe*Ijo~wf8?T^gdM!^WGn86$xvA z>Z>(-(O|-6VQBm`NA0(!Ll3JG1#P+;xn&|H5opM)ncxIy5Q8HsCQ$mIlV7%Z^GlW( zKr@TdtPvDP@`BI}i3Al4qXQBJz;QWob=XqRlYpUMi~CZAfjdUsJ3+D_o~!zmT2j3x zS?J>yT2rBpgcGa%)QNE5*EyT%e`IZ2)DWrgxlVk~M=n(G2(b{XuR3wHT_4yMU=GZ= z&BiG!M)mXivz91vj*0yf#n=m!uPn}wEjsfu1}*IsC$fc zxED3bU5k6juoWaWm~4?qg8au^3xFs18n@amLC>B%uwyem0MA3B&bfjefQ7ve%2Yv1pKBZXGgbS8szrB#N(j_BodGr>XmX4QI3JMBAnLtC zuqT1;R?|qr=kD{J{_K68)cVL19^e=`mye3sXsD6?O+RY>uj2=LqLt)%8m${~;K*~}pvJW10`DQ*& zrx6g?1Sizk!w}V1}c0q$;9v$IaR+S`rX)wYYdPPHq1r3?n$(`R#WbN zQXsdYZSSn5E7FajtH5Z1HT*SAEX?LNbIJ@8#fc>6NSq@1Ke}O5{lC(fa);{-}E*}YA#OK6B!n#3-G_@jJ{h2$*v$D2gNiMnrU_p3BdG@3E&_yjq zW>|4bFY&GJ*EKEv%J+K0J+cJFFw{zKsi@LR51U!>~h;DeH$} zUnI0FkswH72-P51y$F)-eFf7u*dK-#LsIw) zl-~=LC@L}`085BA8sLOa}rVp613x0HCDE zaS{TkZJrRc!byF#p+40x)TBG7tF|6CV7uv$9I|X$49UNx!G1leEE4cF*ng}W?8lby zsFBadQR&aL3eH847iK<)tzhIsFhIH$1U~`hkR?(pZC7S9=?UZ&rMb=N`%%F$dI1EA zKn>TJ4gvv-s-M7562{mkQti%Vei0qWv?TGg1z@K^QthC4A_D7zs^6@pl`cw^4?vNY z{{g*U=;M1*-LG<`_l(F0Mkd#@An_xKL=S^b$Gh51q2>RLKl+v-*b$0a1o*2u<7LX6 zhzB9M^`>?(=u}~V1m%xw=$!>w;C0|Fcc&(Dk6=Mi@q--1;sE-M+zLv250d_MISP<*g73mbDl#s!>Bczj zHix1co7dXfm!sFa%D{pNpabxdb_GE3zwrDErZ7k#x=KZWFd|~(zF0d0GiwoVwQ76N zhVqR#3`BDztEgkuyp|N}_Jw^Gmmw3C286-22NT}Xygl0 z(5E`vla%xblzS89j*bE1KX9Y>C0SrTX`4G0uJ5l|+KcwF1oPxJNNW}<-e*>^pEJPX zCi(-WNcdT*Xg8zcUs45x^2YCa&?eZ^)i{3qqy>FwoFp~SESc{ERO5U|m^cS2<(-7f zFsX!e{H7kKNfuZEx;l(i5N9H|;GaM>c#ASWu2Y-HMHCn;y##~NeqkL@ps#I@#YaJbU&Eng!(ldM%bGjD4r@MAreW14Ux z4b2+ygmWO=BQmN-kQ-<%!|5**fY5}6zh774E0OX|RX<|*njNrr00FIy0!mc<6uuw~ zla$a;E<+Z)!+;yv)5~+#Ltn-N#wAX9MawRG_4PSr=-HBv;Q+$aynhOPy5QGy-ma_E@oxK5 zMM%W~b@2CEIX(I0QwE%R`>nSPw8!W3+7&=lHvRo~{mhZQee_QXwhIb7GI zmw^dVOd_|pBH*pl|BsfqumFyk*7#a7?%A@992GlPHG}gGh!D;@Xm-wrK)kJ+mdm(0 zV2}iZ4XjoA(?AL1fq-y90v4Lu-z=B0E#{y-x#1?-v-O=j*T+}dt zF@b2yi!Z%k$bMKC{Jt42X>f$^{*7;Zqq#xv^B%ol^W+!c0KZnEc?5M|6~ZZkXEcm ztMej09x!)UGhr6`S{MTWVbUSc`%CA6=MzCL)+R|F2>{P0K+>AvRvbm3vKb6p1LYFx zAGir#gV6zso~AyyzENdr%NLko%5)pEb<$k+_owzik!6Re&6x7a5-+?9E8?c?Xh|M` zof_O3_2gDNMfb5Zr>bEp`!0ZEvdbI;H5Lq~I;O%>j`@u^dAuF3okmDXf{@b7cYS6YFeHS-hL(;UA1h*kIg zeG;u`Qgqbjs^)gL614*Oq=JvMwTQx&t*v&9;14^1LGTTG_eJBMk57Lb(e!NEhN^$d z0xqw^xWI&4^tw?MgCAg~3{`DCb!|eTaf*y=RkA=wBLiVZkYBJa1=k$|8`K0pQ#HCP zbcFQZ|MIU4)Omq23+NJTZ_g)8ah3zzx@#WS`LxpBp3nIAh+X6uQU+IMtI5;qd<=M{^?x`D;P zJ)`SGV%xRb$Em_59jwRjfv$HF9}+*-+6fc>x!jJ1_i^c9DM5``4>vQzgpfoqDx!nH zeFUSyJu~eO10ja-ZsbT1m&(k#5A zBuQ#CykVj24x=Gy{1-dx_{3G@b+m9h=7=Jz^CuZT6tSF#_Sd zlHIWYfCl`)D?HQK&mRa2E-SIpIiz#`S<>w17)b)yxTsKQs}uaqh({}Mc?B~^qsuS5LaW+aj33X}Bw*2JLh5dQBaONocmNX_D+3E>bkjp+&gy7SrceG z@H|odL%F;4*4x!}^A1}_B;>tOWeIId0l~W&B)tak)TE1#lP9k?3=dclcQzchv~v@j zI3gr*V}!&9YzC6X6}z|IshdrL5r=zc(ajDrUa2kt{#%NdzdTNL$UAGKQaFY}x?dVu z&@*Tj(II)L{mnM zfFyGX9soj%T|Wmj&T&X z2!ENDuhG+1R|*t20y)EWV#uJLQc?b*GIo@BNfjbNz{H0QbX;n=0Bk1ZL+d6a8DRG* zh{$W{i57QpGQDj=(YSsWURr}x5q7Lol!n_$3TAn@zbALf(qZAa|hta z?|fPS{>H?9`W$QNG!GILl2OJlfBA$d^r2Gtwfi41Ho-i}W9PV@;kxSwESKSZ`>i*0 z?*D5Y+k9Xk&)e_3Lus8StV>0Qrlx%8a~DaK`@TJUZEeu{Cy^lG1fkUtk!nN00$I7rF1phWklp1Th3c{;a{$YGO%sKB1Ty`h<`%82C zWpn`ByMG1auvf9mZnF*<>pWDP24%e3O&yaURq$je@Oz=`+&L((=Npi|nCZ`SryKAx z%bYYq4f+(kVaua(a-nV(;FF5Pmynha7@8XIzvr8;fzxY56C=`q+tx-2nqr&1IlDP>Nw2FF#e{F30PB1`c-g zuS0U)oVDtFSrTC53hCn(+RZ4m^KpBhSz%=?ouZ$25F8}vcmcP&ZGGI`K*v-YS6du{ zjViQV4T_y0dRj3K1a4zPXys2pgWu15n@SftARw(vEne%7HusC*^~UROm~Mf!+4UUg1|eVrwT~*2 zj*Ta6QcZ9}6)RLGtc z2W#O!e*QB{xWG~1@CP6^j?T6#cBbQV)@aeZV^|GF72p+kF23^B`|Ntn(z%*Wf5L#& z+yne~nk1@(p2r*7P2jo!(nS~wd=6bMQ@N~w2Yvp$CnXV#k+2`;oB(K^(D^@!<#wj~ zFFlL+&?*Ox4>Bv1>?QS7mf_fq&ev&<+C-r|&GXfh>*VnFV*laG-a@v#)(1KlIS>L3r7`bqVHd|^p+noPpgI^6(i zs=>e4o?J$6aZz$sy3#GqdZDNA`XP3832o_gv@o2}E?`uc+p+O_1KMrrDuKIm_%LdK6m*EM63=Tv=>PsS#vPTgoC zUHi35!}!WO56y&oVth{4EWksVWfaF)2L{1OO;RigEWFpN1yVX+Ix{@KB=6uUF4qL$ zm?rKVug(5fX4OjizeJU9x8Y$4aNQ!w-DywXMYya9eL$%lj11}OdF;r$Z$VMO5~=FoJ~$`A9v13+J(@wtRE7#S|nV`0*CA6g}l0akt6pvbbJ zU2w?Z>-iP8JnLmv$VmG~nqATUYMa(nhd^X^0lxVIlm#c%1Vt7k30dM&RM~l}+}*ia z-n>LP)a^6}rkV!~c%`D)Es*K#m*B;y0ABNZx`}?E>VuAUc0e2ficJPYI3orU1gcsn zaa2WAsIUo~@-`1Ut%y%4>eKPj$Lt(KPX(}>3WaX^7)s=C?wT1oj(@d%ml+w)950am zKLx&M3bYc9c#I|(g?|Y4Z|U3>5Fh_fBCviwCIyY@B^

    a2lh{go`isVNr(N_*Itv(Jl#VADgrL}_y7C9|0Bt{TmA8Sl7DOgaR$&r z!A%KhOR+!Z5y2v3fwkYC{`@CN{<(iVKg_r9EHUA8KK}TV+$AFs#cGfH{qjpMCtJTa zQ(@vQNz^&lJ)ztj&^K>E{TApn)m|ADg3U^M=QZ@Xhh~VqNjzQlYO%MvIKv`^Pp$r^VkdLt= ziL4eHrWs1Fl%A$=)}lhxZKOb~x_d}lfhvRsz8g2Dx3*U1mR>;BBZ%nl_POVj2$J4i zLmf%cFU?^T_9a2u*|CIaFC*o6egqDm{&y=C@F>LboKr0GA~$P3YcKyU3o;Y$-WJG` zzCAE*XuX#$18tshuQ+G5-J7(~Z@Twu@tJLjG8GJ1Pq-FEj-}XO z8Z)AyMDGnX2v+F=_`UHYRALY-a7~N3=>;#=J)ZS%MCeWnbZ)^5?TGPq$`&Iy&vd)E zr1jIc_2zvh`uR0th!ZcwS@N9sTtnT}rUP_4p%d79Go8)^dT9zc>XGMNg@6@NRMPpD zC?HE3w~cM_!t4*HR`8*?`=rHR&{kN7ou0^fewpqsg$d^58M~*aQ^9>A8Ue-g+-C_K z!WMh$F(?1Op#|a6Jt_LyA$B|2YVLeg`J~P4mbSA2jBx`w{Fbl5@@YzaFq#X%stR+o zAB+fqS3&PLY6NXq@g8g67auj{|34Z8{xRggTFJ}zP1X}-l2z~ii?47Y{^#8y4~kb6 z0J`V^-|$>D94P#C;T+!1s?1&h@XfQ&H+WqU%RkS92sSuA!)XnCz6Wd0{a0UT736V_ zUR!i93?{y@P;hQg4wb+qdAa5QL7{6ZoHmO!KL4C~183I-0+Q*pPs?nS{Y{_N!^}E* zDF0IcfcJ;(92+|@P&&`NNaO_JVM#Q*Nf|CPJEUu?XV`~ zn0VfEUapk@ELZHcILAU`H2; zmlPF9;(0Eas~`#J+`pAz|F5M21NRVEJ;x)-;2co*=kK}SzoY<^V*Fk;&x$s_Vs3+mRM;NwG5FR=AJ7lAz`-tZ*9O5r$}U(@Lc z>(pvz(-RR-WTn7jJmk{9Jlo>ggBz9v4@1j)YV&|K_6O@_QPVoDhPbxEQ^*=@c2^$1 zM^z~K>S5y!yRTVg(J02I$j-mnmw28<;XYj(+9+Yt{hN?!CZwAn2aCt#ub5IGq_qr301DM;s320vd9bFq-Qvwu*sY4Ocj-_q0-P|U z6b*q|*01Q#W#M4bhQx;+#(iu=Qtep`rulF&>F9vanp;s69UA5<jjbL0#91DBEP?dcCkR^ii`L0sk zADCgWdsWNt3QcV_4p*s)fg9zD5SHODv6nW`H&oGBOCh%CU*)^x@d|MoCULe>L}EB& z3tI>}Z-oY3BS?6ZoAY%DoQo;#Hh$k`WcZ zNW%StT&Kglxkg7FoEA~2y(Lv;|BSG4ajV}(OKj27dpRG3s=gKOJ^6Qc2?{DMsK-3JnYw1c@KNvoI5`CH zx6FCB`t#)f#EFwy1W5@zHhrKWNaA@u6{+z;O8noF7w_jD22t@&D*q{AM<;;eBl6_k zJK_1J`}?8IMM2LytA-dA6JE!;Tj8$8!W(^-yVnAlw+^Tan<`9xPda6#Sf0PXR6s8~_+`huNgO-2 zgT1A&u%srG3r?|ab#Gl@Eq25Q1<7DYg~Nlx4B6H~%b`5W^Fu90iq?ybr3;dIL8iOb zPRcHE8XtkeTgRHN;3@fjO8D5PPdEB7yq_*;Mts?|h~olDQXShtv>#U+SQb+t?A!w7bz$(J2X`c8V@^o^wAkqBA_8YaTyC%spbfz0Oev~=N zJ!Da9;^Mwt*R;cBn-%FW~aITo30!QQ}rwUnl^8U_d++ z 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

    *7Dar9MlbF40q^i4cHC{at@? zE@&j^J<5qU*NQDDV33m*)t^}G2qpdcV?gEi5MiCY)j8xEZ2r{j=W%c>LA^t4h~;T_U97xbo@mtIb?YFL0RUp; zufLuty&Tc9pWSmFAv}ua(2HSf#7PyM%`|+zShp!Q=*%l%&fIa1qcI1rnlfIXBUDSV z%Ig>zU_ZnKn=jvJp{Qd|Sexbl-g)Pl()}4fGg`lm4*;CAjW{o~1ayBCrB|0F?onv| zl#dI0y00?O&)=7Br~Cf<@9Fvq=)YFb;KIAu8$B1U1@W0tiHIF1F2Gjo)tdG8xM`Z6 zf=WBa%}Shc`nCzLS>poGX3I9Swl)$U)DRwwX>V>zZ$&Ai%QNJ4fRs zMi0#H61|4n1z?5=T+e2)*|g6$Ijm_NoanxJtpR|oHq~06Td7=u1`-nKIAtGgy_C62 z@DQbLcuYYCrc&bV{bqQ&k!Cp0E=|5P%TV|Lk$~pS5(s=V)&1p-uns&ZRXp$m*0)`2 zWPgh5{A)9VPCFo#EVDW&Rx~bj0bA+26RxNyxjREa8j@XP<;2@_nE?&75zVW zqLeR^m!RR*O4F)IaFR4xV>S<4KsqBK=Jb

    pKM+-p$`39+-s$TtG%j(QS&hw=|$Z zEetU_j*t1uXYQKtOHSU{7Y9H<&|?VQj7`&e2rrG^s8qmPXIlI#( z>h+~`Wp{sZfDOMlA7>A9g7E@oX2(jJVVlT(QiUOxzILfOl;%J5zTI=C^C;>veEz8) zOAd=GHy;yk5R?}=FqxE!&{L7~T@Z5wh9bywK%>Z#)N-@&JEO(9*rVW#{ zIQuPwyTlB>h(~}0$(6`iSBYf+%pc{#oOnbmqX5NmhINq`i!hxjySF~%j5fglSi6|% z*IA(Nw0JB-^uoVVa0czWUcBL=v^~4ok&$UpccQFuOM*@6=Eri$fdM?rq zhfdkUO0^rCwBx(5!WNJlHUQUG#bjOUfLw<6+;s+p3Ko{`_h$cw-asWKweYQ9NArcZ zxqI&4g^p2Jqktxcf;9Q{8q;92ot!uW;KIi)&h?qm{A@8~Pb{N@O41w93jiR52~XVS zdEq&Uf$x{vbg1d^U1FsaMU-7OvY{hTa;95m&1WNfEdXGkQYDZ#;2JXJHj`=*g6i8q zfMqoXV;WKF1mMg^&R67C#m<}wIQE>a0;JLVeR|@+PM7J>Y7b-paIx7+*`?MnN!wZj zTjPoIN$Y8D7hfi32}C4~sT-v_g_;OVa~*H?h{ZPOx#`*f-ZVRL_F~a{UHV660lewf zfj2DF-@Hz4$1xZe(#$jkOl+$R(^5 zS7rlfHc?AOq!9z;K8HH>9M^`y=Te%My6=j&SL=3cVv~l!gS!7>K>DW8^DJ0iZfO8S z;>$IqtFo#(I+{Qnp>-+;8{r*yk(b%Wd+c>dZn%Lghs~}ZrE7h?&D}KJ{zi-cgP+Uc z;{O9_2exIppFazzaW(nB1J?n|nd0E@(e5;L^k77MOKQH$p;vxU53jXgcbWOjw zJU3QY{-*d~WQ?mU-xH&J zuoG~N(S7A@5K9250k)&~B~BumXC2Th6@b$in9q&TfrV?_1PBcPPt3GqUb{<+jS>8q z8F`Ecl0<|sF7MOsH!KA&Iy<9pwLZ{3>+P2PURUqN?w0{JFdj^7@m$C8zWsKg;7p7# z#+bsoic_=mdTD0q*%bOaxbmqDkQf69t%bI^txxQ%|z%*Br;`cM(`N!)1 zzxgxy$MutzH|0EoDqc{}76}J=MydG86PsmM_1YY)2<#*S1Wyg1xpVw17wYC+yDp2~ zxaMV7GBJdL79M;_Lh_2N1ux&{(P7C&HOUL?0}>1V#_z(C>4)!saIwH2U3qy~W)f31 za}^?YQ0!FU*pniXmrz_Lnc*ivH|UJ1a=S1}n;kna09^+a*F%;5zIhR!yPzKEJ^%pQ z(`$zs^U9nN>VmcZ>{$0@SLdS)KJpv=j16Qow`m8N9`R0zCwbh09@ zY6~n)TYzn<>lOqa1~z5`?-H+peAwdt1s{AcFnH#7qgmGhXB(e*yu>WN=MKrH#!nw+ds8I0yj1dw9Pw<=)oVMbQRmLEMi{J~5K9foDEn z4;z3EH(DE<1%D&cD zah_ekX>=AQO}k*+P*Homng7EB>uxn3kzR%;fdZ7T)tmo`G-T8vG@gkg{^V@GvyfA*Qucu>h*R6r9^L(PzDgzGEup#|eV5+PPNn*rl zf;wB7_x+>#6F<}AQmeQ8{f9q%(tP;QhYC7;lV=VIra@IFX@Q~e-FM$=A_UD$>0Yao z3W?r0HPa~b*vQcVp&P(^NtVol=_gH~urpLz=axlqYgK50%INLT4%s(j1tddFv2?QS z0W>_?JZb~ORnyTw=qC_GufP?e)e_JF0KNl^(R+u;Oe2+5PE}A>C^$)283FLFIoBJy zTAu;XO!4owig-JZe&KFbj4f-To@X0%01zJ?C}??i{+~`s%y=r8lBzL^m~$K`B<9H$ zO~BY`zgRt3Fr|LCQ@SgP9J!PpvP9b%ZDnSM^q2*xOpGfG%ttnT>%p|QF2G(^w@un> z7sn~i)6sNq(g5vWdKY}$uU$r{#?2nUcxw7Y`>kiGqYgjfZZZ%fx5shh003}%#3$TH z7K^6P;TvZIEZimMLJOr!U8mMQFCFjG^;X>hR@zNfz&3A#V&GMNAF+Y1G{y7DYE=p) z=CXB0T8rnm&1EOA^8oP_gl1X$WAh+e*>1GhwW5Y`C<f5cA!@X*uYJunCEBkFhC+zogS)_@wXVWh#*%OeV3=TSZJk$^A1 z_(HqgMci)nNRK7JhA4iP%d`y!PkjWWTa?GYBleiOdQ%($^m^z2xYjfW&R_kevHG#w z1%@!(f*JcLtW(=n-(CF6pnGg-M(eW>0KiJe)c_v&f9@w506+6Lzpf9|QRyv<_dOv% zWy9szy4) z)i#zZZog+Wr8?USaap^ zw6_gHObUU`5Fgu8ewF<%qTA#x#OZkpqeUq#Ee9t$>Ldho9#rKw2Z{G{u}p^@Bv&nX zQfkrN4sCp50$REYvsw?D!~h2~1E9Kg{3I{|VfDQTX0>E59ednbJNA#*73! z%JY!~Qt0?vAa0Tg+9=KP(uA6P){+uiuugD^jHFt5UEQ+?y%H;v$2iNb@ADGOO95mZhx0TKOc|MG;^Zz%mDzBhv?WV zcoP9rYVYN?I1fRhTP^}f*NFXr^uK($(w`(z0SKikkfs^imdcx>KSu8o=>Vn?{od*s+8@&jB# z(vkTbvRC0+>7z(`&7FBUn&_=e-dCAylE_i=5!v|}ld*ds^Wm$5Sa>7qTGHzP#yY!j zmfK3zz1>&bY5%OxHjcH?;%eo7;KY_QU3c#k8(Y>}Vhyn&TuovgoA>Zpq$qrtJLr!e zziS#Txi|`K9Evl5Qeu}X{AbYnWOJe*Jw^MwIaUKchgv^{{g+>UVc@;*BgV{lKNg1{ ze)y5P2{2xu48R;`DD2thZ56RQ)o%cHL=lP0006K6IW~%Hy6MDkJ)aCLQ&`TCGQa8JI^8bN%K+YpzPh2hk1yewQARE!x8%DoMT8>zsCHs3RKd{gFcS%K?ySP)()2v7?0JxO6*;K6=L zo}j{f4)|;WaJV$^j{IS(_%F^xX)gH?6>b<5b&}j&_uD zX|i*`g}tBI0|X2M@wWLiO9&A6A>a_mKmYu5o&6_Rf?U&aX7-0JmD(LML@pK-bIg&(B^naRti%~hI_+!5$Ms(V-DT0PcB19tfb+$@EK zx-E`{M2+Cizm-w&@}(yNeagBy02CEo2P=3TluIm?o+=>mOlRe^bHqqSzFq++ENaTW zcQ2B6lq1k+5JbGK5OqK3;X96~t71~B#A6i1o4HQrm2WbgLZHixp2@iu(g7I%_O z&;h$oQY6tFe-W5kuia^2zt80xvo1;V4@UbxcC1L9ginqfIIp`_)}O2W-NE0iF+15K zCe{L(Z_YC>H#aTQQ{HYfV<~GeZEi5Oc<6}h}B=P*Vu^~`c?}-8jCdhYN*KGZF2DBqChhKX~Xm-!^w$9b166jU-g?D@JdZVD8 z_kZQiH`d&^o}Vg+-y5?46YZAftPM8Nh~9to37980&WNBPf@HR zbx}ap)|GqPrB?Actoa<+xw^6$d~yNX@71<_;|}+;zGih;J#(Pei4O;Yb__s)lDuz# zr^rPjPiwaHh=$9c(5CBX09Fiwn9C=(Szoq*oeEmk;)63sAG^)vrhH~YUx6msqBlyp zDINb!@8Vp`eNK*TlyX2FdlPOrX@0PG{goO3=phS34f)h*a8);Iz!tE6U0589ffxHW z|5JacVL~Nw6a?6Zo$y-yi4P3=bA_RXt;pteEz2iYWUujnDQf;W6`)ER2fc82^xM+t zu+#otpFJcEbd&%OV;U!BLZt9O&u7gH)@xZ3yEzdfKlJx0wm4LkWuo zGm?RgEA>+RlRVH>q4-6qnAv}5jRZylA#_I)oIn5hFD63t!;kjfZ!(qtLt_sFx?evh zy3o7c0aWrlT%fa&fB>3+x%w{264wA$fhrn9wJ!>~Jyh4v^3;U|Ns0g}*vIqdrUAhG zCo`5oXRizN)NwK~K!xB8U7vYDjt};Li6fe@Ab#VuNQPy%3 zrn@XQwg!Y-v1$d8v&!=C0J=Ygx#nOemV4i1pqp4i;r_w5(P#RD{GnE#;g|+l(^9(ajax->EUV>-ju#Rm8PE958yy^502jpm z5UPvjMQG}5Hc};oHDEWtKUj~;6-&Sqga|+q+hXbW*GDT{`v@k6YIxKD zKuSA^!qQ{2N*OmM?+ccLF?v?Up78Y}fW%h(d5*|o*C!?QK!EMqZ@f(0t~FzbfBRji-L_uV~Gaw21+b;5znvX^0Nd+g|)xLKC1*gSrj~ za1*ru*$UQkDDX2y1a`9gN8T`ZJYYY7Lx$;Mdk7+ok!q$sKXa16p&6;gMN;|~Rz-Bv zvCW~FrLd*o1_TJYycYgfQaZG0kBifytxf5!^8tSDvwA<|Z;~kJ`B3WL_0fYUnhEHi zeEdo7pgh$s#kni)Q?x@r49fjw4*G>^A9I}<6Idv4-#|l(rFTa=V zeDTGX+PPu>(Dy>fC<=e|oOjV6f;emews-&jJ-wbB*Ar(GkVu(W;#C1X3boXi8m=k0 z3Q2>`F=hZnJ)vK`R4oS1iEVK$o;f2RL=DFg`6!H&tNt#1ZnO=)SH=6crR&RfwZAW>xlHo9F3JBs^n3+)M@Gp?gawO5s{L4Izhu-|JP<6 zc$~PrUo76Q^Dl!a_%ykH${KKL9Ae)*U|a-4b`WN}-9QB-H&)0|IMQX;0!{72&ecLs zi?+AdI)6{syu=E(cq5i62%LWpXw4cPg+8ng>(>BFqF2*Zizr8>`;rQoA? z9w{hIQA4cL;|@UJHZws{EH8qx+zxjH;Q90B};Q{Fd%nPhlReJvw&4b;>7$yRuDn!$!a$rzLrpIK(_+yPsOH(=nv+lQ z^XlTV!vILLHF9d*rKXchs|_H456uu*Lg9u)BVTL zkj%CO+%ZNhN31nN@~%_U&S*%)-}Up;G;h4>|FsMBKKk><)Bdkn{2z_we`LPmO;d>F z$j3AVw}qG*3owZW>a-*%000Dgk^_P$oq(9p4%DrZiJjfC?9;n67`hGs2@Bm2f<^fG zl3=J8a^5dV1(e!ZQyxN|$U>#e!Iru)B(=+cps z)@88`e5c;&Xp^bf?&MlkA@?y>(N)TAikGi$AArH5U_Y3?IbU*pPncu-nDz>vbbv76 z%{sxPxx>GQ=^Pxws`v*m?iG>W(Rt=|nqnN|Gc^M&tnPUo@07}&3%Cak0H8qwGwYXJ zl(SjZV`5}7(CQ0a+v#5^tN^LmVK0AK;%FTJx0iw@0zlZrK9 z8Z>zXZJ7(hs~nfp=GyCs6{(BDO5~n|$Qs8-{=#wVcwW9{#P@LP;ke%N(T3KgU_N>% zqZm_{sj~1c^hp$lE|MtzoN)gZOKVsxU9=A1giTzPQN63eM(m3sc8_FSvMt6gLX_{+ z?eXGz@bg?X04}=Lz>;PIrqQAA%xo`=C8+s_J0GNuP2Qz{Ulp;wXvnw4f6D;a?eAJO zSt@`P5j_Oi; zchF2wt6f*#`_y~#6(b2~O7M=r?aaN&eBt_pcA8_k;m z7Y?KYcf81pzKwuy_ zqv#GnrRef=Tx<7jHb37*QHN$b92Z{A(IJVBhyoi317>XiaYa~53c5l@;?T8qh3=b$ zR6Z6GC+-P=2O|jv0IXe*YH7*OkQh+V;2aE#8GV9OLB%y#SN9;moqAkhz<{N;B&zMQ z=x!vps8T>B-&6C^UUoY@wRKKK|5q^Lss#?>iw;U{0jTze9a|)+=)~LT-&}jR$zn6@ zOW{7Apq^}vW*lDp--K`Nk=gmpu>=rhZ=(3|L4Y^LPWKSUo;}42rL$sOnNh zr09F{^j-nCl?f)BGL97^Rm2kj1gvJ+M|86dtpByxq4FQ=qv@n9RTTGKWzs!XCfSwi z(!@3l}i5cZ^W^4Zr~M+LJkPaaA5*==9k&4hg24BS60IY{Up?4_?UkO|j}4em`8b%6W}2g=DAxL!CHaP5KxNZbyV zT2toFM|25fh8S)$;E9xKho3XmRx&|byAPog#Iwfr!4UwkfM-s@e+7EZ3iz#dy~XBF zW(bwVSm36j<(mrtB7lp<3+s;Odq#fo>gB5z0AOFz6%1`1hvvn=0BMSDQ8goPIbZ;7 zE}W>z(PH2v(kymQ05A&&I+iX1*m(Y>f`eC=rIE8``6R6)#8kMI-gQ|PyF+t2D0YV> z*WL7(mfaB3iC5Bo_fNTdsB(QT&3!=S{!_j02xsi+M_ra zqt4IVt)y{z)UwVYR^m$z!37kQfcU~^)y-PS#Q*N!Gs`&wJ)2r&prE!(nqyBX9UDCh z_(3#`CMhu6i}~WN73`PpoG$GAmL)W#wfTf ziqB9WP33>-^#5fkoRs<=mVG&J+;N}cx7CK}mQn#ZF2)KtPLdg&3EZM{cyS)-i1dOm z-Fl_tRkk>JJD2V|3@Tou_FGazscVxZoVq=uItM*w0{$^2d>h4Unck=-a zPy}{>rFH=zX+nq!F)2-qTtFyBGPZ#z*=rw8RU9|qHn(^9o_}+$DWCChAnZgG}H2S)@zQ_Q;{RkGer)A14gOQ)h9b0%36?P zTRJu)BTd8Nq_Ra$+BlWj=GwKBe}19e?kg2%eXj3%nCM?f}G}_L+iI} ztEd-2%aaoqd9++Z1rqaqZa=m0%F^gbsnWj*bI%d0#HswhupSqhGqht2ZkY%GunrdD zVo-lz%m%dALM}~#6gGe-M#<+h988Z8U6!$nCKI4k>RBMfXMGWX!?MG2Fj2_Z@1`(l zw{sQ;InqdgOm*0jYWFg`1RJrP-vO~54anYM_PMnU86IuzVe|P~uMWw+`X@eCfcJ#~ zCuaT+8Wp&PhQQ5s4n+1&KAIW6!pG|Q9Q)MMkT}#i*`a6$Sb#bTvaRC@Uf`Dw)5dfK zmTEV((9QtYtN7k87loq7H8cH#S4DX`7XCCs?5+{#rE38Y_=EP(2nI<8*FBD_)hI~4 zx&5XCO{bzUuqw31OYa=czZ4b0KKHFIW32a8v}es##lsXbP^4@^4-fjnuit8e zt9GmbNubU+QCO9~1wMO-$%y_@3{qJ0^b)5|{PdP%_`(4&=?a2D6z~@~5l}FPO-#0TD}5?e-SAJdByg}w(W1c?_7681&HqLzUE ze(Am0Wl+7cr5x^RxW0)Sk&JRKAPcw;Tw98Nws#h7pMq^p87TtNJ4wwGCRM*R$XkSA zs@2ZrWfZ6~9f08pZk5q1Lc+SQ8y1m6O4DK+6GscI?cT(pRGMKpyUZ7gyG|PU#}yzc zilY|)|1)FV$GVwv&0CS9#HkPWD7#{Lvudb_F`ARE^$5pBt65WVH}1a0sHVVlk<{Rs zy3+)v5Ma$@4F*L(6I&bKu{a9;;dN0?JhS7VLCrVoD@_Mt&%LNKK#^hMtDq99IC*>% zg=G0IG|u1CNk5bOlPJojR0bcPfb;XEVzyp~hDrV_e|*3FP9yuCHWqkY5p8ufX84## z^P2ej#@i4QQ_RkeW5wZz*V)=lqo0NcF(#a=xvfYq3P5H&VC|wa*QYU2!XQ?Asd4Wj zD#C~3+O!$~b>rL3tvp-egjS&M8fdmNr6+RETwjh+(U;mxdY4{f!oA72e%G|-lO)X3 z8XC)ZE|%vk##d*+AXRvFOm9FDhDjaYkcB-+gPDS=hCsB>hf!C!#C798ry#D3osIh+ z&r^pq!3oJ_Lx(DJ3|K{nYrWTHmdFj7g9K-zFo4=c`^imn5R%(0IJam>VZDeUPW_?9 z{|B&rKdkc$s%Syc-{9|y$P&@uLz)1tN1k)0V7cmKs0v~{%dDHDt3Q2wtXX3hk=KrQi3HCeH`-mv0a^QY~l<9f; z_c3*O@a(>JvfukDkTxj%@o_F@9kfXSwSLYLH!>m?invWUhHTegF>N4lzHCz=iT;cG zZit7(lWTEk>#ldy060B~E(b-Tb};24A*_run5%u8h@=#m7|<@WKB|&crNSR?hD7s} zK<>&!4UtL)ig7nPh)B_wK*?aUZcqt-9t3aZ^wl+O2oIQE56b_Ete70j>;pL#Z2lPK z;$V!FVRidiaaFFj&^GHxk?V-rGP{JIv-u7uQikAl#RRnW(2INw!D30aeD?fk!5TGT zXhce+JE>(`RN`+V0PI?@X3=uom(UgG` zPQ2rr<~?8ceaunEoVx*HPdrmGS)2s?n$Gdj%tqW66aaAvXnxSqd8maF=qd_M;;P1d ze{3`+R9x4i;m{FK+JTRWG!JM>6C;fBrj70Bfb;5QXc)XlyTP64qQuntaH}YY zKtn4-fh5rqunU2%*YpKCp*bZ$(HlN?m|G8d0ZnTgfDmKOq8os!@Vi=LCKT8*>p^l^ zvmyu0YYq|{2{cSVDRNv~7pD6RAj}m&BIQtNhh?}nGzWI(0?<^)>srmWuZ1Mvx+W5k z219B9Z1s&6gL5~8MxH&W!6&1%t>paVpUsoaHWuVz$iRMTOX91h_+W0;-Svp;z3F0V zhh~AiDQoe;GS~s?;2<`g>fA;;w25&qEmsdhvg6ezhjR^@gHiyvuILWX0EkxACOqic zJaUJm(PYQ=UcCxcpcGh>P*RABAf*d2U3*CkX=?;u$mw7|_q!%vrOyHbJr_A-1RjOEtD1h83=o_Gm;DnhXnDW!6VqpT!gO7FU`p1FH~#nDCE!w^gn#U^3iX|SRt zF0lqe8!#UEmrtK6zn=!cX;ica`h1hgw_i6eRNU_~74v0ZOu#A3vG}|1V;e>J!)S{a z8Ub59YZ~cjc??vLSw)z=#&SlUdFq`Ter?N`?>3rFpvo}BzXT*C=UK7g*?CG zI`iHFK3`LMI;{}~_j6Hsn&TpQ;au#v*q$a}J`rP@Pf#@&eId0bx6vK!UTk9nfT@jf zHC05>KL`9SSXRYrCBvWT9A;;ThCpiw*CnR_m~Bz+KvCpkLQ-39<@SNew(%s`04mBL zS|3UV-;-HA)fEMVj5QSG+8q0e|6=>L>w(mNKtH~}jCbB&gJQ9YA}arl1i%GYiCCna zMjJf}j3WAsv!+_3+mioXEU5UsHj{T@0!X~V77#H&*5T9W0Aq^jJK5>=SM~UtC)!+~ z$cGA5M2J4r0h+^n-%*;`xkB*``d&m~KW(l@WE=|r6u%Vqtof=2A6{XT-#Z)oh+ z#Zpq^@2j*r;7sXv1c=TyYR4EG0Q{a~g{Hu9aXl;#;Fu^Bi-s;ik*3K@q@VH+P_Djj zBFk+5ZA2(i{1e-G?OC)+G_9ME^XwW$c^3dMl8m{o-EN+bZK9JsFwz z0!5KVLoJfG2?B%q`Pk_>YTmQ2TFQaz)+~t#F4f>rZKX{;buVgh&$U`Y($@uWwYRo& zO$25w0vN;&3+X=0$s+s1#bWt@#o!n`(j64r_A6}wmt-Az0pAD7pHtwsEd7#Jferav z6Mk8O9B57G3^m|p!5|l_@4A&DPU|07nFLuZWgs5*DHyg7Y+2N-BPc9#37Gts)D!?) z`1zt()@ipdn1L{2HDnEd`}ZGcq7gV+yX^UPsl(cbOYXob%`e{7+(;IDrsM1z-(mL4 zG=QR(gyih(W(t1B#nXw%z7`A8Y0AC8_5Z_9H9ql(-D7NNQjI(cNr%OW?d*|;deJ6C`c?& zT;?3!coYE1)*s$qnL?X_dmAS;+K6a%UbPDz8vtlWC~)@6Isua!MPi5opqdhub_0^)hKdWChcG{b zkW_{MD7yhux07|`b1|UMuqsnR#Ich+G>{03tC;>IrcU$@nKHKa&|VAH2o|o8(;aPQ z%39I5dEUhzvdO)6SGBH1S9x4kS=h;uo6px<{I65mpI`i6j}c}u&7KsQP2^l`4;j}e zVxQ8`)I&ZrQcZ|WmDK%O-0BDynglCPl7M}x_nEg z=Q+*Hnz5{bG=p!Q-}VLM9bNz5MCTsh_B6dsn2#d-=2;mb&xIDZTFm=tJ~53o7U(&# z2#Oo;V{*Tb>f{!aR3z8OG8MFq&}EwAY(volzFADXqyeyvIs(KYVv9A#Lvi~;5d@$X zXdaw<1i-o33KBc+-%v^bU+MybLc37{CW^qm&z>u4Zn-`0Tmy2NGNV9(Ot{pB%_hL| z5f9u3n7GBQL3Hg@UF8B7Kq3U(1`a1Nj?w5i_19lrnEd@C{(>Nc?xE-iYv^BHC9&qJ z4Ub>*xz7=qn}7gN_`h#TT}I(v5F*We4g1j#Y34NTc|#$dHysiZ)dYYQB41NYU7`qD zqkqU`wC`WpzRS#hFp3aY5v9{W0L%y|Z6@SsW!B2+lI3-zS2R$X;${`f(rCamR58{T z(5^j{x92L)v1Sy5GB9*>EjXp?Ca8yfg+ZBrmir3p1^Lpf&O}z^OAg7;FIv0ko~igR zJ&l0;Mz|$(fqYv{Zip=6dJJ>q+$i%wR^>x-bj9Lzc35kg05X|u$l`NcBVQzwnOU;l zi7o(d-ULB8gM|f1-+i5Av*0R+yXqHln{Gn81fg>oFN%cdy(-TKC0^~kM-Dy73&GkJ z=)|`okt&*<^o>tkKJkHr#O4tYexHcoW^fs&Yag@Uyp`{MN^NF+GV;CcEa=#PjB zK>r|609=MlC>pA`nlSie#k{9Usz})0cwXl7-E%!&91KwGO~{Ta(|m|icbl%;YQVIG zMhn~IzN-bKZv=1*{6Db$s@;_6c+^T1;EN~<#0nAV^Ket{C(zIL4H3Y4nv))*7CK+D zpse3gBLGg6VsN5AYYIy(OEg^E%7XPpCMZBNY&`octJW5&3ktY3x_z<=1u?K_$p_FT zl;ezXYrpOmrAvyV7XIt)?Bt^q@p6O-{y`RguG>KnaU-&UGTh%=&@^)s;)Hqn6=+G} z_@E;ZBj3Kg9}#j+o5pCra6Bqlie5=sTiL3BU|c8PUtDUOBMv@&_EejtSp88XD|G!N z+kb2*uogmDW9`OPkM*_C{RS!@#bXf3z0vSlcdER9N1X(CEo(9bJk)?^e%}wCJ}6#) z`AXxaHE@ah#Mn=KEYtNC(K zX#Bj9Tn5Y6xLQ*HY$(I^IE}WH`zp{+$}&Oxyb`7{R8`RYa$^>Jk@Tw>rtxJM9P8k;^H`4@D zI-ea=md^#>IHq8g6d;uZB_Lr%Q6<_zt@=>=Q%*ngSHsToaHH6zqrre&rnnm}cxVtEUv)V}pcaVrsI# zzt>|zoatQY3}x_Q_D%y(6G+yAu-(nX#k0?$cK!DOHc!KZhKSJoiI7$(iyHi(sEqw0U7g?J7|FV+XJ5n=zpy>7|Kq zWetFRQv;y4yB#@rb#SiZM7XZO{*i<*w-1sj!u>UV-sHc{^F;n9`L!Ic`2TO@X2`YJ z<$dI1>dp7$qu!*D%w|daz7`H+&0`G>)~um{%1?@8Yjm?_g|#YMD-~{E1PDBn;OHd4 z7tLMl09iLpkTr>jMOqYb`d&nt*@XR_a`T@mEdQ1w!|rRWW}oIx!1|0(;1z}5<%Kri zpZeIwnok@dZoc+K=&d(FPX?ME%>?#E)0p@`{9-?#98^Hq$0O4N;J!lR3kbe5ZRm3z zpMCbZ>J9LoOKk`Qw8H7>&x)W3vF)M8@ol+n@~7RJ4{7EdpIvw5x<~XFZakZ9sl~t9 z>T+xb`yUG`tD3DHXM$jxYbZzdO&)Snb$>R=A~857`M>G?xJn|#jcYSfeaNUne%%1j zeeJ76+2Gz+jeWUwme-Q2<7dbPR~AJslGzaDD-tKalPU~N$7Oh(Nv(@aaM)at>S2RS zh>kCB26v>1V-u*#&T)XZa4_}l(3)s^ub_|}TE{2C6R?bj^w>LSlIidicHZK6UV_UO^) z#n)ebt?)#k|8OIJ`sqhav`8{Yn3&2_*j7>3z+j3}h3O1@M5g>CC?q<-_*D$hZ|`^I zYrfEAfeDUHs0~G+VtCuwlqhZo-u{E2Yj9JA21PslY?E!n+YgpfL2%EYZ~^vLd+^@& zO$&~jM33X(+L`(@+j`@Vhen2L2`J@oxm=Wr>kG=`&h21-WFmlN6oMk5HIAzV0-Lti z_pe4N@Z6kgGvi#7%#--OluYa*oHI)#23C)#1}$CqUsSyrHR04$2x>$Aoo?qeHV5oz>juC!(DZyvpmAXK zaoPaDW8SafI>|0;c(w7~aUbCDDaL7R)8J;kfAR89MWvY#tn&Fm8$>}^s9iKSnD!GR zcpWjwXl~ZD=XF6-;8d^ac`WcBeep;e1{V=wkn7lhmSelbX&M-LGu^qeqhP+2wxkX; z2ESKWz389;_`4Lr`VORm^6ar`)BK_z@hU5xKIzeqsAF2YDejW_OM;9;iHNyM)ez=u z$NF*2am%BO`lwdJCjpIMl+fG-pzE~N%qtcNXf}rAuBiJLE;VK?=8Md^$}&5CHUz1y z5YJGCRo-|g`dkiVxO#0h0UhBLKQ8e{?@lzcr#?52*Nmr3Iu8~2HCTi{Wjy)d{}EW&ed*ieLDWu0T! z{3N>x8?CNuN^&AFZ`{-X@LvQXLjdG71^sMA$XS{|XNtcBauNv;0Dt@TZz?WGvck0d z$&=qSDf;~L&x&uq{Zq`-uhOe?R{4WAXCci)6d^d*tw+ zW=Wf*=R%T2AuvP4nt*AN;Ltq4rjTj>Fv>6<0 z;hezHry+uHBMBeJs`oc?7k6<2RUeW|pBwAX1}6YX%@oNJW}}XjF>%)ETK3s=s~cuA z5|d;QSa~f)asIxMFex)QiuGr{yBLWaV}g5~1+*Mq#7ff&(iKOZ{Ez53toKVkX7MjQ zIz89uR4^FZW^7vyxE7;?ei_a^WxjSp+wN1>rlN}LXrNMQ42x*C_i>VsW67m&CI^ia zzkW$s?4vq;71u1*O>|SxV=?Z&Hi`Y(4s2eh#F6q83b-oi%n)0MD>Q{)_~tE*=@&kZ z({KnVuB(Q~nTr%Rxc5MzU--iRp`W@Fmnh^x9x%;+^6;_72Za6Z_$ILl2`UxsJW$aP z_(OAt<0Q@k=cnKYU59g!SnNRb&>#OTwd?&{kBJ5V6a?PGwPK&x1M)tOkNuuRH=E!S zAE(28>#a&YZ8sOd1pX|1LIz`ZPIb_fd4`k4$;+ncsB1eTTj&jv!Sq`@RxMkkq%E+p4eZ_zNp=mK z$99h*ohd$v6jO6^2>9~LuTp~{;}IneJ#hU`e9?}cz^6XZVfqe7l4M7l0KTC>(gf$- zyplIDXxBdQp(#P*;r@er%G3YdCu<>~?{qsH)4lt_@qh`5B~0dUU{er&ueu0+U)(Pe zBpMi8I})jvKH1~@jaoFlPqc&_Zl9~%x(NGIC1PK!--u2GK=yeS-e{8|a1HySa9(&D zoK7c2zHxJYGl&%h40h6{wT;@>S0oHBto3MXO! z>zj$FzK`o2zc=W;6;`?6nXSnt&Ife<^8J+_2Wv3#3%I&Ey+l5J(h-QiXMI&=eIJkb z0yO}?{!R3jr{K0Gb#g6;g(TE8;wACN)cO&Y1X(dFc5zx@5B~8#;7|)tR|-ZbU&a7I7iS8W<#qJdI^HC zWAizgY$Vo&UqI`*L|fxJvM;-jxC5A_p|c{@;^>s$j0~&bi)84>NF_1-9G(}3G1hqC zo`B-e=SeAawT6%6C9U;0oH_oC2a?*lx3&SaVNbHi%UqJAP5tY`|H@(rL}~`q;sKc& ze<4F=@WTO3`T}2lrKH_th|Rq4JXDEVI-#l8;{wpN8g-fWz#uMBn)*3Cx`Ee7(Ow+u zWii0=8jdf&Jy}any8&g}sC=zmCf$IMPkaea!%*8;^qx&p#A3lx4#E3SG$ zQEs?|;B&PL;(yB1zN)9+YO%K6W|Q}7)#5Yv9aU@sRLLW^F7}cIPY+xw7dJAl8Ug&i zFQ~u%`Wr3Qnf_Dszw*LEGy>r8^KS(Hn7A{krf|2QXS{xr6%sOve_;0{H9!@^l>DVO z5fDC7(L$yhr?<7(H|U&kHC?pEQ$aw#9kxYs@tR4C-yZJ=j!D8~r~x|$iJfVK2ZjJ$ zTlewvG>S)EzQ5EY>ZL~k+`aocc`EC920LZB+qA6XGiH_I2_A`Xg&> zx*T>G185Ni5!oKi7_)*(4S-Aput8A}Y1ljVB`Jk8DF8uuu}1@74M0(jOpRYJM}K74 zzkCcCl#gf+xImPzkm4`r5O~eakabM}`EaxX_tzk3PF^KfbRf#Cc@&UP)o$E)rE4W? zvKHe$2JK=IoHqc>rq$Lj)e-RV=KkvkI)&#MMZ^|3KJnO!RN{%bq?pJajbn;w3TBFD z;+_iY`l-B4=UHxd5n-nYs_3W!1&juO#U*tD@*{>+Njh(fO^6Iar8D;! zGyy2~pZnv*;tv|a3%?yzv8Ry_-2TR?((Zyrg4gEpf`IJLVgu*;tB=j0d3>6*aki1F zm&4JQI<#m}?CPndIn0z%6cAy=eWX=zEHqz&^>Mg2wcu>gnDVa4_*1Q$q5Mpo&>8?- zqkW`w@G~?5T8i-_F!SLdjdVn)6;&5w_a-0w9eFRi2$bVd+H@etHLlTph0oZCx1*(IDdWyO@Uj{$2o}Q zX~LPBEb&Heq@nYmqEY^RHpwcYw={_m-!{!xzusu&Q>z-+TurWlA~96?dXpLe@84%G z0Qp3v@6nYeVJ{bZG|{94iH(hte$U*w4eJ(M(haK#$%vq4(V7a$jAg>*>rdbI-EoTl zY#1`YZdl^jBGVP2%coE+0m&YS->}fo%7R~J7XC9(7H%Zblnw%Z{p4C^K+g~BC&-|C zi|bf>FLSApJ`D~x!Pu9+9As^%?|4x5oTL;oPprM@$T_!Mu54{qHFt$ipZWq)w5iNW z#M)^ETtEQS+AOZ9J?=w;HrVswB=Gw6{i51cio&9>X5FO_rQsW{QK>PCGcnOs*1-q9 zLCqBZj=B-N)ioJp7zOqVU!3!AVgbjR7Z7Zl_hLE7#GB(Kj)-q=t5$$-fPabg-@f)P zS2~)HXY#p33%wv{3j%@o0=4al+U|a;`@>du8=DyF`R-%bCvQIqR)kSxm&O71fKR+N zAapol5js5TKoqqX@<#7;6(YShv)Tx#Y8?edI7DqAUZaABhNj1+}I2aJgScbQ7Q8>8k@7b$m)2!KWdVzj-|gHVv> za7rB$q6nySLmq-dP0%JPQkr7(y-k38xYCLh8N1yrnf%MF+I7C7|6~_o8G^naC9)k5 z($gY)EU;g*l;mK)sP}HKonV4A)-xtZR20&guu31XLgt7V_7^6B?_Q}0PgU&) ztj(lZV7a>ctN}ejGEcXsQLN}p3w4kHsQIgSpD)^9ym+aO00evz4HY+vs`XB%-!4tF zxdt>7m~?#e&DT1y;EN=ILi;BVlcWG=f&xN7i}y`)!lT`E{~v`l$rMS?lSoB){bztq z!hggcIe0AZYj|Nw7(-^kHDb!dIjhK%-|rV*R7sHme%k&z!az-M>aI_qxSuYJe~Nt! z=y|+WDnRs=CgCE9RR~FCA32k1qRDVXW*DY5s=hK zRn0fmO|*}qk%;N}o&Y~7oIagO#0oyxwi#PYY zK%Sr2qss%N9`x63-(Y52&4fR!w+7p$LGZxS2Z%#9HD(*nebX9FKI^2a>&FIwirnpj z;|-1EN8T3jwW8#nX`BNE06LC;gDfMKszsd78ORfeF*JM8JW#7#-&lX;8y3VcVEs>? zJl5E#(|aF3p{#9hhPB&{g$4mJ>r$H~JB>5A1e~22`tB2r(X;kAI{GbV&SGsKyxH=Cs!Zc{LEHyMlzX)~j69*SX~G zq>U!qU*>e+dP?wRDZgu>4{HQtC7JfnBprNQWH7h>tM})6H5iHsX@S0GdAkWP;ZWCd zgco;U@!x0g?TQtR1zD}P*H`#g{SAP*Fp4F50V~HaYcY84I^Y%368W3zp}B+W-M^p# zgB$+<9<%trK5v6*zwXAu#3EmX`+>jj`t`V5EAI&K%P+rbVnkr289>5>(4Rv5eS#9W_jGHo8bN^T zK~aqLAHL2^W)SWp>7j_{Z_o)8N#LgwoE_vVfcGcTZK5_kBfEX;2 zaMLDCWF*2`r6o>z@aLw_#!{}cN%;XSSdXpRzjSnaRNFck{Jm?bRv(oYS$t5w#0Rwc zD-Lgne@KJG*|s^yy*N#gP1DWUCIlB#iTAR{p3+R7OGa|p<>RjNrd+FofXI@yB7)S#ES{&z$IN+`GALHwSz@f{Ey)%ug z4-@=fMFEv}A4CZ65xc!3z`gqqG=?DxZ2AQ`^{?CPlacbTXi@+q(kOg0<8DMZ80w8{ zNNhy^fa44e0MLDITJ?MmqBD=5W>4r*ZKug~DO{JG9#f#M+;d$|;RUmrXfnn*Q;qfQJv_vvb^!HvqaaWwpb>pm*RADT0j-%)sWS>hB|SE9Pxp zvW{T;gXvnLULDBf0lZl0wF9XkHNE$4NZ^0wHEMfvI@j9g5!0W1;c4hqyJC^e%AA$o z6}7TyfM#E%qWCIy_N@U>TM|hojMDePol{ImKO`@=B#aP#(*cmaF^sftZqI~QYEc#f zDHDV*zxvWo@V`+=B}&62H7NVT$^Y)}-)Yb{?SUfM?(7mY4p^loI0)^*ck~7OvuDqB zKNRe28&N^T^a$bxD=YT(y=00&pkrT;zx-8`3iJrL?lcAPVKzG?CN?$|kg^upGj)Fw z6YK<`6uf%%l_ru1H<6_M<`b|xeq!{ww?edQP{fIpDItr&?W&;k8x*Cz^MP-QsCAUz zo^c_y0r29bCSJF1GlBPs7}Su)eQHi#U9@c?sF!6^OfuOwx-yFMxNLGD4Wvq6Kx2DB zwRzLfi0B0>ehfweEO^baL+fSnXtd~(`Vu@YqoOuqmDqIeu~k?SQ9C- zX#fD_4?=cteN6d1X$61|*u+LfuDa)OrW@Cx=thBCau90;6%l5Z4H0rV;3U3Y{Tq(an3;(0i?mb zd-v5^FxTqIr{1$tb(dQLR;ln-p+EosAQ3o*fMR%~`yjq6k}t*{=XOQztmFp$kfp;3 zke+_B^fkBRwKbZEHUbo_Q8fkfoDBKQ_3*t$s9<^Fs-l2d;VMP+thd2Tj>2#YKG)J| ze1yhX?KYJkTwk+67O$^TH#jo70q0k?g@~nY>Hug!m%qu3wP-pVHX9CQ>)*}gMh2TD z!_>H;zqJLG8xQ88b>y(k+;7GRAR538t&j2uM7a>~< za^Bu&qqRa?_uLnLK;29OK&RCaQb>STD;nd1uTuDD~T{6x$=dJ$2!1cGazpx2OsUCqnlCFg$ESgZgrVBvXn1t0> z5!V}&+>~I?hz^+)Bi3daRbU*tEb|y*EnN^faO%4>fjStub%Z8BafnFm92?SsG-%Fw z9@fYn`@M8DEM(-QMP{H_dW5p)PR?bv#*9om?G*DkGOkC9Vu-SmuN5*Q4eAB+q~StS ziz5~BeL&-hk8#tHeL$$qCI8##OCN0jc0`seHh(nD8xBN;tJF`Vgs+|Z-yiY zE;qpu0BxIh(Mp&mH?fu&i@wgI=U!hR3KLo*o7ezij%^Wtpc*{(@z8{Jq1gTGW9#iZ zcXj$7s9*Q>8ntuK&`h2u>?#eLxTwA#Q3Yo{zOya(_MjOkc-3Z?+(^+Eu687{9B7TO ztIMACgq|ysz-?)c3uzd!j~Yo)CrNUfx;8d`UWF>HJlJAbsoyTf*6=wnie_^VB5!g`^^Dr_SQfDyI`vV} zXMLFJ7CDGs9+m=GB~ITO>ben^ybb_G`ospNSp}{y)=58KaP+g5#z5qmqvSFXjh-B!u@Edq>`Q+JqKibYK!@w~8+R>B zl%*=wR@AuGs^N}^>y$?Q4+JkZ#j3i)oZHQz2G4|**XEWt*J?M$@^>?9i`9l#bzVn}lG!qTpb`XT#C09pWi-;XeTw!?(w zr=NcIzkj5KGRFn{UW<1rQZoXGD3D2bo}c>3JxzcwzW6l+ci-J7$@<}kAA)$^NqDWP zK3mcxyC6Dcc84TuoU1}NJskmh0^d*yUi)MwO#PeFtQ!7SXySEs<@6Z1-yi^hU2;5J zE1-vLi*sSR#H1&v1#gP0$On{;KXw!vjQ-XEzIm$W&x?1_C%dsmw40z=p4?s!%GAX# z8~1$b`FHDazKd4ZZVkLb>HIGO-CK1GN1_9K$n(F}eT~@#j#6RdVCvWPn*JS`^07dd zLgcWnmdlE8=f*ys;}~6+qarimveE^yRxReTLau6w*hw_9_ODKsd%N2^6{TZ+XB(jr z8`ezL&b$f0J;-ThyoUlkSk*o)+~YzU0URfyv#Je%hKw{7;MtFtz12oH4FsCqY=ee3 zC<4=!!Q%DSt@9KkjdsWHnbs?mKROA7=EED`lzy&_avI&l7xqy^njTt1XYQt+n>pnC z5s`2-3zW`={h{AC4RNS#G{_qih}jQt%OxiA%Q=7Y9dp^?C&$CJ;uvw)sS<^wYzeT* zVs{yFSpzDca%T=Vs>W}w1E2tinOGG1zZ*56YKZB&2IlFI>2*Hjb8a8Cnf93uK>mI= zI_V#p-dJ(2md{y3UosRZ&sHz;bfT=8g>cHW4NR{oJP(sy;@O;w+F`)Xr`fcs5k%5! znY|GB-J=(GJ+{6balxioy$m7fhl>BX+5ZNR3UL2nSW$vI$c%i?vOF zaQE%Yt#nwQ)u73t{jNP38d-!R8pHxC+;zKGBt^g?3F;KJOx+RQvk3))fE4u<+lU1M z`Qtr&QCyo+Fv$lHN|F@#k~9mj7KFEt7QmU2CII^cHi=R* z$ko>z<}VLCgznN+9Q9z0T|cKCCkU~j1J z$Lkm1|5vdAaFXDTranwlf<%zdJxbAgr3Rd6gF+M0zNu|h2e?fX807UlYrrm59Lpy$ zJV&(%R_m5FI_4vPD?X$_%^GnRInclTzoUTuZ?gz&LdxaiBn#Ie2QU(g*74@YY}JbE zXaP(k9M%du<}^6sTogUacWN;s7XurPcg;zsy_dC$sV3{Q#STNI1u;NrP$ueWXedNa zBDX&1t!z@!e-)inSU+=%w*31v>Hu7L!9HS)(FiESLB0hszjs=ATTIbOXrygv1L47a z?+EZn3x68psw3dzfC}^ZxWuI2a=?Hdi*SA)J2))%MdAM2li#9<>2q>s_TYJ zpP1XK=2LT8+Cb9g)9^$Ch?czEH=d_=8q+To3DAU^j~nwz3!T^ZVXLB+QlK2{G(QM7 zz`HuXA1Y$9WV$o?pW%KY4Z6dw77P|yRzVHhPleB^1i@%?BudmubD-+WITtnoa(h8L zCvs97Dm81R>D~0|5jgij@`%R%M~e6r66^%5RMSKX&m) z-q!@Ux~_1^pg{EUJGq+oAp?re5{pL`ODjICvu#1i9_|^aof^XJMg&44L}(ki#EN>W zObIdlLXRktDHrSXW`CstV0*RIbLM!Mw14dd{9eUE9mwG??q}uylYo8o z^;a5{wwmlj$o{?x31-Hd<6KFeIBtH=XVG|+OnqKIc%^O!d!sazWr9pvXF6yAoapq3 z&l`yws?KlUzKh~dQhe!cqV@{i41!meBuWVpvj#SQ7Pl|f+fR4nOjL36}3e}nyvkp(`p*FYSKblx(`xvE}jPD zaK;0+Dc7Y>&^NV~BOHfxC&tcoe=kd_dshsae9c3TG&!wqw)o*Vh~BMv%%ny0ns5b> zkqUo$GVeMc5f^D{M&r?I5!aitJy?j-K#Cq8cpLG@<$^VJ^lCUX)M9QMZSn?Qd7)5u zUH5!Z%R0Y_&L{>AZlD@VS38}Yp6EAmgMx+kqXqEp-FaB|GevOYvMbuk@0-`cou}a5 zrkL>wa4<7V`ZI&i~r2S^I5||2EdbV=RV{v#I%5ld#%lI z9A$_dG$UvrK$AO*jdfyZnN;<|HDc0lcDTeJ&iSpUFR*VK5$d$#5on4U@ZWa+wP$_N zO9Sg2$ESQKo$U%}AWh=sT58`%J*5^1%++@w&9%=pYzF@LrDcNWi&xEY?uy`m5< zJS~BIm+OCJh|R*aEaqg6V|_=AP8xJub5siXwYo*5VOurP>XvSP>c$M)x)b>@YztHe z)oB=ueS-`gDS8B>YU)R{8cdCkcy2PFJ1~Va=(B3AC;t&W5gRt9rBLNs4#oV~SX0`= zkG5TWKRsVMwcr#*TUv?CD13ij9Cb5iHLjMHo^;QMGD*sNCc_EWO^D`75;82gaCitZQEz?pdwk!<@RN+{h^$dAxig&jtfOl3a;(rY|&^dla7CS#-@##txH^dY@r?P zM!Hp5@H|Vy7Ima7%NE_E9dswQogHgEjd3X{;>9Ew2=*ZaNOA(tkmH0A&_KWxo&9|6 zodr~6$@?b5f&BgVKNLm4G0_+xkpqp(O*^kUyBQX96WVtAVP}+9VpxqYr=W9fo?9Gw))YN6d+u00@~k8Um+%-X2*Esx03b+8ODev0?A$n!TUp7vqm)UirVHbJ+A zZD`|mx7X7m7C`G#IQ;p|4hqv0^4ULVwL89`zJ2?S>b9`v@_rNaV~uAU3Vq+!D96(H z{2rY~0|5G$#Udv2G!K~YgG69Ww;aq64;&A%26viIKK)EBWO*-*_g|vE4em9>IJ=gX ze{gL+_3vTo&cyp7PP(~n&%BcXdOgI<8_^=r_E*N{aVX$olgGJvf&fL`1W;;Z6qr4U z0~u>j8uRlLE z`QEYik{i&P$kEIxCydW+jVN(5o!dGKYY)V})AW!b>iJ3=z=OpEWnL4Y=uyq+blXB- zAZ#+^i_)P0TsJWE*xNrUsc$U@#Bynusn;5u%)D)reMwq0YkGivko$$*hB4q}SzFIh z$QbvniazhE92%8U159cYKg`nT2+c_zTsCpDK$I88c78f>?V&!%a=GYuj-_7QaE{(~ z=Ji$JYbV%rtpwYFoz3uOY#0Bzx;^`$gG>?j_U)U@1>h%RHhH|m%CN)`M=BO%ZW{z%2LQY>P?*5~PcR6zFk7Ct?R6v@#~KmAzz_=6O$ z-UPVhACaQK{ZU~;399)x0<<+ZtTAybY8Y@{yoBRoTQoHYHi$73&Xjl@(HD|J8Qz#&hWz?F;y?RSi>n+`& z0{B{E6Ilg1yWS8WQ!@SwRq+2R41i;E5dSh?GmBL@25mr&{k$H5zVIP68dj!a`7@Ps z@io?EFDH$!8XxDeb3_S!%ouAHBrC$6~-9Sph2 z-}oKtG_ZZ83+(sWF#l5#1VKnw>1PZoI3HId7ZAcl_5X8^7C@WkQj7R^8jF+y;Qt1~ z%|z|4;xLJ(1n=XEoa9e$g9u`Qct#_EW;WCU^bsuAv?f3m>6xM0>6?g;j(-}6PQ8B6 zFiuY*TBFc7$a&~F$LrLgpsff2(i(mddkxuV2-KnobWQFzc8xS>h^K}mF~^}%o93}e zZ+4j!y>fa(bsESQ)UGiiJA&q9HBivYAT6#fM4#tnx%J`An`g<+lIxFXuq(^PWP6tA zUW;N;IM>iGv*(898vEt1wd>WUA&tzu^{Ph&bp()*8V9Isegre7LGwjUZ@S40M z%;LFSk9}ZZac1FG{+5Rhv#^K4>9%$-8w*sLp1bailcMI%Md2L&O%Mo*?a#Gy&A*M) z%_4|hjTLHqs3|&jk(1^|>;W}INZmso$7AqUcviF8DUxu&+s4r_=m#e-$YKN+5`Ft{`dd+zIf!t{DLp?Cv_k$T?%qR zQy@(5nWg~$N9ahokN*GREOK3G65Nx!3Hp`06KI0?AQaew|I$f@svOG=bv$&7=AxQUlC-sK}NG zYY3H58)LKCbemx^sN^{g$y}(dw!8^2qklWh^(v-~;-u($U>v75JEU0e(|oz^;{Q6f zKg&CLEfIZEoT|kZJ0!R3L8rM{Ea}-T7|(TLj-*Bd@fsu}*PJgzA$jIh5W12q#Jbs< zizeo4*24sQi9(R%yNE;Xt}YX@~H+(smViDk}O49Op)76wkavJ1tyZJ%qnj}5we z{{L@ku6;6WyEmy_)}k+OjM8yan<>;Q4EM;x?DNO|OouC{LM*k;9Wl6z$LNZl$g-yA zI!tdLSB4zdfaswp%m|ffTa}r&untB9K%X*UviqCG4$rL)ns0FfR)1}-gq!kf9MV?E zYyd~l6XJ4vtVJEa84=s2j%5sJyox~f^DjRa|NDRaFXc`X6dw2w&)>YlI#bmkPIjZy zp%jrpT7Y%WSx!xnH9+9^TKFT325+0fh~EKg1IqokzkQpU0a%UMA+?>m%)_v%IbOdAQG`eQMHB~?F;6bbuW*%V)suIN$#YJm5X zuyB8Q-yKu%*CKxtlg_#c)288+Iz>87-D=)v+5(^(NCr$(AURmLj8+gS0QUK0-6&%6 z{RUKnKYs!t32G5P_j^~hl&PZ@N3QtaST|)M>~XH>>CDNhj7#AhtN5REfSDytL!eUlZ4JIE$pL*Z}YtE};?+ZubM%2dLeY=&G>&cshs5$y}e4S5L- z2asldsKD=7Pj6^J<&qOmq8#pn_5e-V+qXi|pu%r{f_@o=@zhTC)_Jm3cigb+wt}f=x2AK8#j@P?+;P?L^4nEko7Un)K-Sh9&;?Q3! za-uR|)uX*@pdg@;!#UG5c=M``4z+mnsL@J$G~Qqy{ zMw7wbWr`NlS^qN?qf8UWfa@EjW!wolhqn&KJqQtEs1)2kMde8oZa%gqwq>$&Ehf&5 z`}xT0{RHvKpx1Ejue0UzQuNG#=1PVd#io+1(RE9oF>5k$47a10vK8by)N|57IrLxZ zCfcADh%1v;1^Lv)#pi(i6#yL~x=+;lx*EEDvJ4=6Tj|NPg#ivRn6{*MMWrj)_@EKKF^-`^!TQI*6*nuW4- zJb5b>VF~*JZ4ktGSokSo{_~&z=ufuChlyh$C{geP^;927c20xj0FIU93Nuy;h z6jKU4i%Ri$y;H2PSg8$f>zX!N{Qaiw?qjQ!K=0Z{mEBY&NH zV9&B9IQ|)>BM(9V6@Q!qNQz7sgKMh7g3&jb`+3WEmw85wHe-BE3r2`0@hp>&zRze0 zKq5gc1o+>rFumW^mQXH=MsnZS&KEv#b1oMbv5DZ%hwDlZN1MPPY5V=ITe#(((u#CA z)x?AIpuxueI4+Jc!23^=AYl@ARI!`j4q&R%9QQ@^hkmVU#&2Wbza7pY3KP02vl=a3 z>Wu5uZl5a??~`5780^rw;{uG`qT+gC@qZ)`pM0drxm#&l_@Ul&Sk5DdUF{Efyhou7 z9_lsw!6B)Gg;q{uB!&{!w4i?XR@cYJkAICKr%unf4`8I#72f%=ejcG z7i@#6r%tP5Gg=$l{`fhLmoHxG?}#IYR3JtvrPE)xsyc~Ofrlwh2c4wg;)8<^2uc1P zVslxmx0mC-?lPF?R+*sTR-5x2L!fYoPNsQg6tR}<0)tDE?fcM$*|}O6(&DkvLC}4` zI`}Iu8ccCc6U1C5t{UhESIkJiI@Z>eAu*!{1Y(cY0O8-naBBb*Et>6;7?F@J@pmWj zIe)N!p!*|FV?R7U0l9b*V@nl?-ua+-isqLU@P?Emk1@nDzXM6aMnax*SDR)w$Y?0v zH|y0*oxeHEZMsDix<9m~Xrz%Bao@rK$#VM}4ZFu>PC@hS;}4C~qm%!cMYGJXe;u!B z#p0yjpNrA-$|>&!1UkN%$)mVonC?ra6}W?;d(q$v7Og?{uG-M;n>z0 zo>F>EozHARvinDINH<-%b^iQ7J#Z~KKkLHQwkWj?OFey+);*nBOgL$bxRMua?984< z`x}Z>o!Zj?@aOMhLenn(o5oH(GCQVwun)Qp*5M|&6AZYgUwLE)g?1>s-$xX{4;uVg ztO>f>WCA|AuOa+&-@V|!HoriJAW%et#&!MEKRYx4%r_ZC#re(igk-1%GsE@ccxV7n zpudc01nvjNMRH{g0-Y?KhGqy5e{Iynz(W$pq~{BtsHg|D|GYf*?|%^$pTG8w0`D%~ z1%WB&1`tD4!}92+2Cn(4^aP&(I-ht#Ip8`Zq))W1EHbnP6E6yVrJbl1 z)hneD(NN)d^m;~-#men79gzC-ArZM!L@q4}b!t>h35vPJ=H`Q-`(h?X|AW7HqUCc+=4&M|?6w?|Llb9Ge(>{p_^EcutQ)AxGHxqX_`Md^gfG^*Dm5MM!+*Of= zu&E?M&u?iVPaFgi&KeG^U!nMZ{inH|Q_=zgz$s*$h70d`ssetG6+N(zO=M!oJzvCc zqw@@LB+&oD_29ZO==n0b5pZl!70HQcKwt+*1AxZ5!S)S)Z`U&+He5eaB#(hPg@PP` zcuj%E^)90oBmWN~(ejjrh2&T&E(23Pmp?__R<)lm2!{ zn>H{6g`X8{GjGy{;7Nq=z<*aUIg;@RL!N@JbZ5vikZ`*M@3F4(rF!7LPm|%RSOip7 z><=iSii~xWq^FY~ILrj;cWQz5T6udak{3OE8y@5$lhFSmIA$K0su(;2OfT4^8SHPj z+ovQZG#gyg;AFttOp2~up?1=*cFB@REoF3?97Jwdur6Y!{whMtir4_!FA$a)f^tfR z!cZ<)6g|9wn!OJIoIC&Kc<8uO%vxYd%E_^docK@0w6#~hAa7&$-XQZ7@K2uw3Pj-a z|KS(spZwoG^VF8V>GhxkPqWFy?NqP8FX(V2vGPO3pV$5J%dgr%LQjBmB0-^m2U=>= z1HNcT@Qm5S;i1faDg zBIt2mP)$MtdDf)mSScel0)XW0wu&Nt>JcMv^u3C1@Y^BzC^unRlNtb3oc^2MN3^#* zRSf{q=j_iLf5z$CyQJ|^BQvsQV5Z2d!7<}h?i2N<8ic;tGT5~QoU64egCE@aECOlS zogS}Pmqis`gOM?2J^ds|d?~qm-tXaZTUq+ReIL>zm})tuFrC*@MP#qX2A(XD2FZ zMib!81`c}=KYbs?b}f#SuC|-AA}1(?he;)|T91#otTFjg3z&EBFX}-{SvEv|?YU#$ z#&f3le-f>4DdO+=Vn-+I`#|$_o0_7gXsdD;^loT~_+l_n%iab-KYxvAn4^0`%(S78 zT@-6ngAjP|;Jr4lKm9aV<}&>Ufyi*hAkb%qB1nUuK(hnp59FPq``z1+3vewMl7MW3 z0-#ol{_9p022?a!Y#NKq(c^CxrPcr--rrK~gCJC!D2dj5E{X~6b)Q$cbSj))W3`s< z62DMX!r7TNWNx0HN9}|-sG+G}lr3j~c^^HOH9+h$qEuHn>b^)VlF0W-66bbqeFSS> z>(J;j0lxkIJfU}kAoN!M9%_O5em1A&GVQ41x`N5!=UHm>srce)$m#NC-XLvR#$rVi z+Ksj5o%pO*iXe4*J%i@eCb=whs2USCeRJ{xHxmIfHT7>(L*O|U364MY zvl~iuD@t_%+J;Gg5Wu^t15QH;fBn>^wor`re^RAl-1y`Mhu4WfEIRwWW>*L}x!m$7 z*b&lUM`@OVlXK=;sQUby!28q1IzsZ>5T?dGmhpz2(LQWaK;Tg;K0kT?^UoK`-w$Yr zw<_$%1fEF;F#nvZrRvE9ifFe4HN=YROApoj#cO0R;o!c~VJ3MZ34*&>nlpyK-ij!R zB31YOq~=?%qhVH0LHYP)6hit&>%P$(nZg@lGlwl}6F9>7$q(2DO@z-s z4>9g#M0$ooJn;BKQ)8C|6U_~vjPVg#C^|R9pq(Q70zJ)WZJ-pG_G{C{=kjJX%ip*# zN?-7?3#uaeHgEl4gg9hs_*`GJ#8olq9*SYV5noMYH=#u>29*MEQ=9KaCSpy~h-ap$ zMY*S^~2i_rltG^X*Cnol8*_lA`{*HBi4pSD@JolO-ZlJFtH(|51DCbXU#{H2AxuUK! zY4BRrhRQao;HN;mPw0xSIMwhI+l1PW+vBApt0W;hD7viM-?wj(^%fMi?3vwS&3BBd ztPx^QLP4=g(x5>vTH}~N;8G5h%g)nyiafIv-rs!vjXFOBkL0JtyT?r(2nu`_aOK@; zqvkYeC2&j>+*jXSDZDE{_5T;;y}$8%;h@oIpiutZ7yLhY z3IQkpCM|z>Bmfi`lCLwh@eB4iWhSucVyAf%n+|;6zI`k41ev-ZcF1`d=h8(j3VRkp zl5>@wNoxS8{S@3$zkj}YMmS!O6PkF@un9)h>3B@I()^TFz|jAW#FgZeq>e&QJg)#ma#9G%7)l#YA@A{H|H%kXA*&>6EQY*RBw8riuYOw_*CGfxUOA8_ z)LB^-UyFMDrZg9TvoM5-dC)G>?##8JS)fkzn`aou;8vHfo6z=y2KG9cDtL${&_yW5$AJj$dXo#IPQu9DioHH;RCjrl@im#Emlg ztE9%$S}mNVD1qWw^<4?ITt)Y(yx*bpxU-wzHfNm)~c)0rrZoVatURSnkZwrt`~ z&|$kbZrxDcBv?i6b1Fm8OX+cW zbZ|1m=Fc4y+dPP-TD$FeV4oga(9C1bxdS{BMUK*#?zb~KB*~OTiSE#JX+Tf9hq>5v za2VZ|)Rtqt@oD6muHwlT6Z8N#ASmKi$7X}5-+;&q0iGWw&^^YXz!;aY`& zDisAGzmt>9KmLwF$ls}cfT$h;h4?GA7Q7j^=Mo3ngkS^Gv(gwq46`Dgsk}A;e4K_( z7h*Iu&Nf_+h(>@<&NXcoC;>LEeLmPKS|YSn6i9BD5Hi;lskeHOloKX3|ExjJ^nVivY;Nd@ zY~|R}a$b^=vAi|Wq0fa>&-LgE$cS+Hf^}IMU5>`|Jt-nc2F;Zw@}MtHOhD9|nhUA1 z(#-XpxNKZ`XlpZjHuAb+jZpiF!e5T%O4GlC4mCgqH7DH&EwZ<2kS@MzX(qPsHUQcu z(jy8uScvC*fOzqx8^2}RAW8;}6J;bHXjNM>i>J}f0Q0nWmL7sDnxv$xU6B{!c`KnS5x^#{s=9BGYOD}ZE2aSuA=?yb7pCmiBpE{hNJAd5XcYhU9-9srua2K2W=!0J zARy|{kM$US_6Puujmf<#)B348iB8bL2$KpXDY>G0#-U;eAqU{h#PT)*_s>fiqdu&6ka{PNiBqSAJ#H)&34gV~|6y&~F zDE#uPFZKN-Y83b+Oh5hfvvTV~g5{eUr`y0^)yb4ZZfZ!RfI%{SBH_HPjs~T2F@fSk zmoNx_ud;?|M`TE$fO^;^Ef4e_c9B1t!yS_ylivH=#TVZ9_)=Av-$a|PPc$(#+o`IO zfrfC8_>&w<0e+-F9|Kygu5ahrqe(GMx$0eQXBPj*u-Pk)WFL44szBy8JKT_moy;%J z+g4Jpzj?6Mu6J?qzi|69Yx*v$8y9Zq9*u2#6D&aIb@jS#9&^Nvc~dnjRU}p1>Mak{`+F-0OZ(>X(SEyzg?^6Dl77_?RDDyO<3+7>TkBGVk$l$pLjIWht0p0`^fh>|q`I!yrkmaN&a z;PWR=db#)_b+`X4at5u)rOSWWfDCs1sohX($Th_P?f=W@^Bcr>ZbUfvi6#mZiR^C^ z?e}&`>?R1Z<=1{KX*6*#tGbaxpr^}k#W}(J&zK}TEB-gvYpR= z{#@w;s323wV`CYnFt>F(6!K+kg5&}<2uD%qLm4133SxN|sw+T~frRpA6irg@eYKC@ zRZ^H`TX%iJg>WEA5zPY%cC04361Gqz__#~Z5l#>&P_=T zb{pC?Y`Cb`&*W-TChFN$Z0b*bChS-g;=f4~>1|m?%i^o#3eXmD>!bQn$e-W375&dI zwCT*+&GepF!dgp%psll0MuAykBx^CRML>{AKDp5AH$gZzDEZ$Awryo?-D+`L3wRXl zKY6HVzu$j52^2q%26*~M5WFR>ZsPRa^1Xf3h~d~l5xi0@o0p0rGn62-J=G)d+oNf{ ziMgBQaCwpEMutN}Q1PZl!}Mo;(R1or4S*_W=;Q+%G=7p$q&WkWuGQpl2YyJKju!2v zx?8pRnK(7_>VZRYYi!phisR&ZFmgNGG8xp#bCdxJ#X$|DHFy2ku~-gY;5+rM8{}Ed z-sW~v)vq|7A@CgNqR3L69+|1(IQSPgfEjf|9@)(x<9Cuukg=@N{_W)_n1Z(^{u{iqONEEWUw>ne5bM;A8bVoi&6 zB(-#fq6a||pmGFSh5tvfo}fy6l|csG4B@hjOO|yuB`WSKEb#262Ha$|5*2urr`UkH zg{FZ9oJ?#_oJ{H-6f*n?5)EYG-(e~qrW_*BLXx30X(^S5xD7T;!;3C@5#wgoO?Vk<;O9cd@kSmC}XV30wqZqWF4QeX6 zN->6Yn>d3XI>*4aDA+-i@qSPNG#AdJFkn7?*9HKMUyq7=7qw_W9sKDV^k#Yco7df; z`A#l_eVsMbZ9T)j87_%c(nPh<->tpuv@qwpRpRIzv= z2&;|Tc9RNuX^Rb-LT;5kG+CQe7Lwj-1b`&&`e}KZkO)DgK1`)aF5de8hn5hmcO>J9 z6(i5xL}#pYX_YY{!XgUsQM~UW*Yfr2V1Gw~^rIK$(*}S~fH+Qt`A4{-2JR&0cf0uJ zZ{O%-#Ox}8Swbz~y>QtP&m-9}zM~=^e6zuLaKP=q^45a50bo=0;?EaBRM5YM_kHut z*Zw%a)=A1H+17*!Q|W>?G!ye=tfLP zK~I>#lv(J6Nt2951j#~*%;3qpRmt3vRk_{7n=0C5S)37>ESrkngrpxH?+)mE$Oobk z%)4fNs?stJRh=rH*A(3|Eo0}K11=Q?l(}qyVy=Z)m((bO_UB7fI^E>CxaAdfdG9<*V|D_%x1R~i7U?bwv@H>H1ZylVR!V)R}u5rZGR zIs!cUT-R^n3g<%%Al^K<|FGEkDSghbRyUXJCTiF0b^V79fcW1_fBaXm2?4kNzTZFF zf3Sa0=Lyj+k;Vk^hu0I25!wX}z;z|2J@L(L1bsPAkOCH)?bFmx8|8gbqf_PmeA3j#$b z@-sCDrl?c=$i3I8$QMDRj@?|v$qv^PK_aucBRL^a(+Rwv;)5n&y01wWyn2$0O(>8} z*oi{ek}Q%d-t+j$V=a^s6=af6Lx5v3;ice?;FGnO>=|T}0$)W8&(5OA<13Fy_+3#6 z_$kvwprB65{ka`f=ydUHN)C8T_`}cs*z$TIRG1)o4b)J-<_wjSGi;WR}7c>s; z7q9$8pTeKxv<5)O89CnuXEIGO8WOhVAwg9bOod+%4Cl?C%1IarZ+Do)FPEu>yP z8jV1X379;i84xSa?egf9zdLf~VC$9+<4kkhaXgGg{j8ImS~L z;qaFkwa!FS$BF<$396+Z4FF%H-}132Gv(iRL0wOB0x5P#FJ<(4co&^Z{A|&;9$K`(t?Y=22>I^TVT zaM!=Fh5@>n+@Z+hG6cXT6UZAEWQjc_nenu>MF3r`GhhMK#Cue9hkXw4honcdcu1Hi zw;&8t&26wu^OFPKuKQ(z49B`<8;j|zF2}NMH6qqL^lf_^Ep!p$^Ua4FQN)PJ z%2$3;&*YyZ0~E+Feu{4*fCi^59)xY0V}nu&yr2=!5y=rs{$}IHc7FKbd!3^5I=0U? zw4vbd%T=@+B8efJ!978NCQoa^@Z>f7Bxre%<~Wk~`&?o+st%_wiiJ z^ExyDf~Pd+VZw{=6hituHNd#8Mgg$oH&H+0q|AgzjY2cHg3HGWtcO>BB65Vq{P1`_ zdkZ3~rcod3Ypx^iaSnH8%Co}i?s!bq^^>ixYVpF)lw0H!)$^9Ibgt-)#S*96rs?B4 zm>21Z-ob7yCGu@3a{>gqz(bF*riTC5nkjue^Y-?%;k^%QAeMqObon-0x_7aNH+xxk z!$m<~p90H1x}@y&JZVbYj8?X0;b(3)Py%23qS|tRU^Bam?m&M3Cf4iof!k{7_o+4? zXl`qRn?l}GX9_M&N{6d|U&SfBHfyfTGI*Q5je?jr zj9dd*xS-K#jjj+n8%@KK~UP z>2t(#`x+Lq;-(z>Jh;W-qMvdz3jE4B@iM9_7`t~>KLQ|w{=3Y|PZqc!^r`;KjE70q zJ#@|$ow;$1&Fyodb3M9F((t;HgJcBzivP~TILA0yCggQ8i#(mk$Kp7j%o_!u5-GZ~ z`2Gj2okWN+$Ka2xnXl;d4gP~fJ1GwNx z6yZ^!wX4hM8@x-lGo55%goo3y9E{4crm|~dgQ&5qn2>+XO%=QIYQJ9ShwF`15JEi$ zYb>H@p*14Xb_Y!t2W%Y+>Af%nSN3BTO6-e!fHffYKbqW_h~LNrrE@RbkSkP31Qg;I zmGN)g5Y3!5nTRHd@-*!NxtxAd!Q?TYQmX(@%0NY#P_W> z0Z6n!bTG|l%FWcBd(QRdTJ!8(xsZSA!w-5K3VHOL zOeh^?Ri>iYl3KOWI}ZX%Oiq+v=#Lf7B!~v~^|hxPplv{c$b|}e7gahZzDO`RXH)QJ z;z9z+^Qn^|uLAvefE8o4eck%%Aiw_F3D+c2DI zfHzl^Le{C&xS24!F+rn-V@2MCBw2N`eyr+-kKN6BqZ|Y;J@|EVuImVlvLVbioK+mb z%yGOyGQ}em8SmxyQsmqC$`L`yRZRkEqi19$EMIz)kwr;w>w>vK2OoQn%TkY?A}2p4 zCmmOSfFKO+J`}l8_@rpMG)VXJ=X##!JdY4Vn3ciuTN?n`AVIBftFEZ8G--8G)D#6` z193?k&(VfYo2-8AREi*LGBEwvEY)MHq6WY|Y_C*-S>Nov;~M~XUKKZdoFbp0;09qu zQy^@o%05R?4L;r*b>yJ|;bR3txx^?%%e{N6|0Yl1cyMemmL8 zP!qV$?*lQ5Yk}P@=c0M&X{kCjHi-}SN4oi!jA^z=FDlYi2;oR8O~pZ zcyX>6!r^!CvdkI|h!y*-xv%v2bBS~#Ue`}PYXh{jByuWWCS184u``(Kj4^dXhW$K%HN_{FfjPd?e!>9`5=ZOYmT$0z{#^D}Zc z+bBQhTAh32T9rY-Cj^eLC|mF*FeoIA{pJZn(bO(*6X2<&C>uNDObc%0E&%@D?`Z(M zzf1*JV`8w-rpT;i0#rOqQVmk44FKP@zl>_ai11L%QKa%Z0xN|c|4(qW_qC}SOVwXp zRuL)WF5{vT3s0;flez< zPc})GY7;3T)@Xn-z;?_78YIE@9udGXBHToQpAZ`niC{|oJg*!NO8Vb@_nn?1jRfqM z{{8QNYf^Wr!b#_v+yyGZKB8S}h%EO64Fo14Z{NICN(9Lx6DXcpLCodaA($ zD_&X<0B}G-De!EgufR5FaNWLrOQ%P{6(hiBuT02l*Y&wCs6wjqL9={coA6~jY~DMZdhy;YfTaYMJL7dzQ<3991?1w0L7LnCUD zdJ&3LTW7WL;HuQzu3RX*i76^Dd^ZT7dj^dx(hiihcAlENgGHx@P zMOBRXT+0lRW^;Pr`qGTTn$h+v(L&LXAH+|!e2l93oyNw(;(wj;$FQK^$1MKkD7rmI zp4uOYi!THIi*vU>Ux{KS>U`uyPGqQ$I~2d%!YkwY9?6V_bC3<-$I}3qd1KZ!yw*SR zZUzOQj|S0+i=9Ea4BMfksX*wYn+=W@p0U`HPB3jXh4SREb1kF+l9xCiehU< zRFMy9w2V=%xEue}(}cK+d`Aji)aU>4kAEl%f}jjnkz|8O!;e4ypUqQ-Ve5ewQ_Q1e;HTSlT&ZiSl&^1 zBn=_X)9ww;8n&(Q&*;`*+mL`q4LnwC1@8ouf=!-itXy1(URRI-iZnq%J1!Wt{bt=% zNC}~9cKwbX*suA0B66)P`u{ffy$=b=ltQl6R&g*)%Q2>9hr|Bm7}sV%N8@@vV=@=B zIc`Cl>6;kLo1f;$(r2Z&ru~=Ph2mkCnH6e7uV_ejzy=|)0A>l9B~Z#vb(m2^4FuDaA-?o|ra}#R4i{MG=4`^X~~Z zgip0GPadGU08hieiOoiB%ddMQhl?>JXcmN90!rguL|1Tq)P*W)C*?@gW$Tr6^ILHZ zi(IY`s1A^h+z$@tG*UA8CO6}nSnhb}dMO`p9hGaI{Qvc}X%2VFk(KnRCH4Ez#B#2A zu^c%!8Pai_+UK^d{+|N@2E-Wp6c63XM!y?D3Akl6%i6!lXa~~o^yd;RL{|D$N=s0$ z{eWebf9>Kw?^sL6pVUpd0_1b^)Y*)1b3*`ZvckeB0Ml^MVZD*?w<;;@ml-_%R43g{ z*8%;GMNzu)JvKh$Pl-!5fEEat1`|*p`f2dfr}uQxr*NZ40(MT3WV|}_QO`N_Zlbca zWW;5f3OE6`ma(}$lX zF8rG)cJzaX-lNX{%P&6{|NQ4ey&i0V0-Eb)qKdhw;Kjw?HK3E!{25`6oX-#6|4@AC zn+W(Sf99VpULSmzV{=7lhMX5)efgCpdYZKQ#P6q{e^S^Z`-2OKV*tF!(69;#n?gCG z-4xpZv7iZtzlJ1OPxZP6OF+;COmDc~2AMP=KUV~iN>%#z2?!fE8z(LsLWMjd=nA}! ziHo^7Bq1jWNFH+w;6D9%>$x_*j2DYCHRqPxE+o^h7&0LeSv>V@_sqrx-iJHxZXjNc zPcCH{q}~-R`?SLfJn7J`+(Jd?K9s`8Po(CFE6E(!yA>iu(tr~us$*}FEXzhPeNK&I zQ}anKg7ixC_8zayry0(M~`uo_#q^K^UzD;m4 zaYdg-^(?c%Okj|gYZ0o#x>i8ec}1AEOD_aMsyc3O{LlGV*eG+-}p~=nmn6NhiFyYKCKXTFS1{A;@FCq(fp9VZ}`k7br|AWHY z#|)4H#EH}lmf%8kwmH+kL2E-4mRQcd8AL!MAT|YEnve@@HaD~52(g*JbNz4u=ALda%DTpz^l(aBx(Yeb%pFO4 zV068on`X{F4jG`JA7*1aW&O8i`_jJO&PgvK;LrxqK%jR62)N$kwa>^bk;UE>-VJ9! znah2t1qs)e3nJTo?Js9lv}DDuo4DvMqY+4U6J0SKDm%7=^H2xUmzcQ@oWmt&g$CT} z(Bw2egn&YA$xvwckAzV|#`t&Lb^>gaUJfvdguf&BjcKXl(gc<{R#)l)x?0B zE&$UBm1IK(yLSfrCXkX?sO~{*uAIh1h5rxJ*>^$R{zia+TT@0)XYZQc<8Dr#C%p&& z<%%mEVxxiEl=?~1=;ptC85#lD9pU#I91Awtq#_SY^KEMk9%}d5nomrjPf^G}3DbOv zc8k|NcMvtZ*EsS%bvfBe+f!%LcXl!^0Gz11Mb5Cn_`%paYO3aevVhAmJ&Z zFeBgGC-{;<7vd^dAo*=T13-ZvI>I;K{Ng7f_cS30$dwFb;=j&roGIjzZIE32{L|0H z51SuU#sAr}=XyTR{dPa2*%OfvwKb6>`$#5GQk`q^r%9wwj7XBSQ55YGNzAwowQGoL z*a`A%lPr?l1PV%MO3>U%jhxm8Iz9J1qcTqEv;LzKfsfvSD>BGGBWmCq0^NuDZogJbFY~ zqWERbJN3|{Xf&gxHNg+mvKz^{N6~7RK}1#g;!M}INN){hD0VlJG<^6b-e3u++d2to zD{TU3(fd~aPT^d8jMwY1S`D5C%T|hiCiuif8szVkhyO=(atV0^uTv#{&3D4_k!L|Q zB-WX=Za|P->hznYKlC`uMJT|NonMS2<~9QwkncOu-9T*teUlxVKyoT|R=RPc=?LKV z%^bjm<1$U7or=>21VCuSR1tx3Lu269^3YU>4(P1NbW5N~0oW^_r%^(D^%i2t=mD&D}5~z8I)? zW2Risa$Vn4dX$oxcMuxS!s@Vc=7xB8Pn{yypi1e?CVm1|7aSLlK!zT>XuJ5oT} z6q#gz0+S*Yejx=li3a=CNl5Z=mQ=7VvRY{&0CbWPlMLYTw{LH>0RQg0Z9pF9bZ3b>AVAOGuL{|d-~6U2UkN;65;E@I@1Cbevbu>-2rGS|>U)>-6owUN28^VFky)U zDpY&7ILv@TfWJ;0AqG%%D8vt_(2HBnLld2nFFB*Xi zjes^iE7NWGH>dH~Ru!SCGezG(0LZjpl8hMy^p={J|4}ue?sD>1ogzG?`Absvx0W&~ z4Wxl~dQn}i5CB&$sutET#sibiw4iQaq$W)cGNR~Fb=xD;Hy|LMdEq3Lb&&DW*Uk~2 zKv+ynw`$e>Kvifr-{Jhh?+c4NiblH8Kj&af_nQEu#jj5^RGl|nY&Jo5DZC|Z2p&-g zv;M3F=dRQ79Be6?S!?)(=0SX2sSHbjDc+R4-EGcTlYwLeh(FU2rtlmq0(unAOyEPX zkI4tWuk-{>vQBmK!+Md(u#FFM6X5s!InE7q$5&r{sh{UF2=(DCpv89;2cLhOw*%*r9 z4#$n&JJ_e!;K_Cy35V><4L=-&@^vR*GVM1viq1$gP$P$NuaJp?V?4tUIqcIRTX#@6 z5sfs%x?yRLCsFp$9*_;Rh{b(#0a$hNz*=NiM254MxGyGaQ8u%QG}B}fYP%($$0#-?k?7mBFbna8;an!hrjB% zy~sK{hyoI;pwFp3gXb&6h1kl%o6*)p$aI2?BzW@RiJ~Z>rO~X13h?NUM>@DD=nk_- z*wAsR=^>y)E-qiW0N}B%t0ECj4NYKF&{apwF{ODA+@LVGw3gF){NB`2Dk6=JQ=?J4>p>g9eZ3cDkHlt3Tw#L&v{}z@xXKJKmPi`&fs|3o-flP6rl?)3qPj?3 zIGq*$#*gm^^GTWu@z}J;^R7d9fJc!K@M=@#U$qm;ltwH{Ccwhd(jAhk+^Q8ET(8rU zA|9i~c%H(mU7lOC0Q-*TqT6vFpaaN=Z*o6DaP0x>yqnz%!3Z0?==>I2n`YB`QAV%j z0F2V7{xSUzc=law0GyXDIO200Btcqvdigr52EbxHY>9M(!drC?npA8bjRSY4UGUVj zop~&S!vDB@E+!oN_QYh9qXjJ?t&=8Thz@=Fk5Ba%L!gm_g@y#8dItYDs3MB^z!JHR zP!?E!5*4iYKK}S)O|n?aufP5(7v4{Da`Ba;3_fjMe)y43(WTfE>C`4COeAaw6Cw&o zg7b}96FyT*K!saos3Zu9H}kP3cOq!22@uCa!uKdQ8aNI^nOIYUhQfE>8G<1iFgn3k zgp65Vu3S@qe<$gBoR9fZH3fdx>jGNi*e4|+T-5Sa*X2DSE)>W!#x+!DhnytR&|^ES zZ6Fv5MPu|npTb4SjSK$`-hWDasr(iS)xPxkPS-EpdhvfqG}+EJfV=&}{^vfH9B;8s z9CN!#eeZbL=N#u2+4CXw;{x_$znJDn?Fn`KaVuEa!l{>;3fnuFI6Bg=3=r08 ze$rd2MZoktoCtpV-L?rz?J5`4oU^#Y!KU7ZROr&{l4Gj-6itki<1!R#lrquyK&F?9 z&p?wU=R}9Yg1JmxE7DVkJ?J5=TYG4093A_>sM6|^RvkwUV2^h@J!+Z3P>`w6Hs98F z+lwOobtnJ(Hw;1oAT=F1h>0Abr}dV`wcxs{4|}-E^&gJG0Zy9s$xVZr0{#8xS5$tZ z>o_z2EG=)jMU-YD?$Q=_sWdWZkPf$cP}yzZL$?em9dZDOz^Vy;5#}3k(kb2Z(X46% zniYX2Ci8UzASUCr`uj%k>#UIsyRlT71@#M86>J@*Y%l36=fqogHpF=9b!e|kGck=$ z8%b4to=5ILP%G^h58+CrZOm$Mi!N~o(GhfjnAV!ga0_8KmEGrh4DiKYx*h-p_n{TJ zrLs{Bb!0Cr75!0E$2xaXEQ;^6L}bM(7@J4Dmb_#Ln8A zS*iw|trpMk>V8yw(seHoT0A*6cxYcWyQ|zh=d#Wlz3(C-lx_&^mcPN||Igaq+`{;_ zxe_vg5F9K1XF3PozHIWoA3Lxkh1>*jIHx$7SrpVf)z!M9@m;SP_b*++pI^Lk*A$fd3q^3z&4cQ9XGeDkdK)l% z7t`bX$!&=94>Hp2>fu#sUbM>j>!ReZO@!kUkb@^LX>DRzSFKd66xOhtQVb?s|q=!>P(0mB{Y+Nlh6p1RXBz;*J$lg8EjC&x@| z47VpO`uX-;utl1t;q>-3+TPvngniS2^LF9Qe>2$s^qQru+5y!84&A#mkN?IN43mC? z)3I3w8VIFjPw~Iu0wCM*Z|Rm2i+MH+WBeew*;=PPtcmP76aa#Y zL++wL+2xLs)S>w`wE};!AY2|5essS95K6ZFiNQwWVZw5+4qOB_5(?udnov%N@?AMc zl4FQ9x8Qo&X+-oB3S&xLVv zO$m}4nixPQIkz8v{6U)tOhfnrULRckxE@+u=Ysi@T)eCBo?0M1(e2Pch{FE;4?nO8 zKAy#?rOxop_7GnB=%cG5c=T0Hl-LH(EyswfK-l>$DjTOLzLn{5#v=`i0)ZOK(K59~uK@eRNuya5emKUUTQB!T1)q z+mjJs%*0+sBmQ5-*64QW2l6Mo@b1b!M%Kil@4m zUGLh(snv9cr8ax-Dg^(PBO7T1g8)DWAZ&rdg0Jy?)D3`Rr@pE1EBZHio6nV|U`+rh zb2c!!GUQj&PT50~05k*Bdm7r%(1@c?UCkd9fC>QTSg8XUSKqHir-=1w~21RWdG_}Qa?|ExeQCfsa$t!bXT(0!K zymq>Sy6xnz5PTfcESUX>{zZIZ55KFKR;Uy1_VPEjzwM*PCmT^S1>$EH{|BrmXWS^# z6XG9s)aZRJ`~04<6O>v$eADkf4eU4FtV6URfi_B^bB?*xosFSHux{$q&@%d zOYgcL^&oo@JXLhZ8vvZEzMQ=wqR=uKhP4bTC-(~RKsl(W+_9_kFH$MK)u%U~vvTqj zhBw6U8OXb&)^YF&xtRS>OkVFQf4vpnGkc^9_laMYKuMwyRy%Op)tR7D(Qg$>H2A1|+dNle$FsBpZj!`EMbt;IXEh#zu80z|<3AH1)h3oU|WT9!3A(&_zu zy~nwSNxEpJuz&7x5-HPJc%|1k#5NV-g=;v%!j zFFWr{W(1?OjiNT!F<}z&o61By8tLJA&ome`U+p@QYAAPVnmUW<%P>jW;#?rzmr|ik z2AWB_1>F#+L@Kf1f+AQ%&Se&@SE`j_Ly^drHu>nS<((q;Jf4MX4exQCvb*50H2_|e z=0Z3xX%l(1E{Hh(NA7UnIs6aNjlmi=$0XKS5pF)kr=UHm9DCw8h|lMD?`VMy+KxC# zLxLtS+W=MeV^;I0u}xgL30u=S2LcPcC z#2`*hTPfO6?f+iBD9vl5!krZ@o1oLRjhm(`Bsilit@Ac<3r2N6@YokNjoKj3YjNU& z%&o@OeA~Zm7Po1p72a+N_BSuBWeasG$+%9u+(JKjqHf&#^J%7N`w)%3W4|b^IP%~` zzZo?YO0}h;4AC*lyD+tH5*c)aoxG1^nh!H%Vc7Aq?@j%Oqd)JHswumi2<&6ld}BSe zwC!T;Sp275gl+~4|FHy7o9dUUdDfp6kM$78qu_#L zR;!eE*I~1>Kl>GlcW+GOLkD0jI_*>)0J^nqNi#@2sQ+_>^egp!0R*RU_r5{u_NkhUAJ;6O149ernLGjuB`{g=nLXZ=rg(~u!&%G6UHibuh zm*5mi?y0iSDT>3jLCy?Yz`99yebpvrS~R1M|IiW)3eGRS_(CV&94~?WgNzV}dr`%6 zM0Tp_q>;fQEee08=qefoY&8FjD{lV!02k5)Zmo@Hr=+3o#nlbB@ z8wnfz)r$Z95(JsNI5$I2q#cJyMH9wYp*EyYpLJ7N?=&b~Y|vv_L}B5%`x=7tqp+7f~=IV z9m(i|7fgponK^>9C3x?gU7Gpv&VTK%dpF}y!FDh zXRxy+2Qmks`|4(J?ly#`gVRz+(k}R*D%T^flGU@HIM3hQ!9ZPqurJh{Cz|~&%0otF zw40qd|GR*q{}KxPGHtmiuSwYVrp5oEEK><~w% z-o?wCvP3q!DTkJFo~nvu_nqx|N6pO#f~TE&aP^iWZ*cL_04T-rz*D+y04M9tWc);} z)=ml>YJ6X5rt7y1YGp_@^^L$nA#QE}24TLqGzh%;21~~gL26-#nFE%(n;fvSq9Cu{ zwF})Z4G+Ep7S<*>!_BFF-=WI|wB#XG2mrWvp>GgtOyTw5{zILPu>P;SNaOEpk3bdO z@leGr6o54Ep4y-Qh>7n7ypQ90c=k}I6&xSMKY!X};k0=F2FG%AP4GGa^TP*ubVO|dpq}R zF}gcQ@h=C4{onp|ZvH$Lh5Q-MS1J=Ug|AmmKH;oW;%-H2jF2vkOrcEoB9ipcf1N?W zL~>MHrA862@otei1@S1-uF}Tg>)0s(2myxPicP>@YMd_=iKfb{s%-}fG^@>OJiB-A zuKvmUoF)r-(lh7a(;!$b7o~`As&fxpSlz~bE4VqXplYa2&M#Q{J=9ou--TuM^Q&v8 zM-W$&{v3)N$TFt>S8kKY-`PIrsrG_-ThVxqnD$(C2aGO&r6Emwj+fXNR=#Qly|^A8 zI&HIIuBdBA6`hq>_v^3LbO)_KLx{lv`z6MQ`$OmsbSy1zQ-9+x`6aI{G##QqKg>4z zgM=L$II@Ef@($4cbmMq)Be8)r0JKuKl2kPSD#XY}!jQIf0ON>XmcY>#os}+b^fe@Hp_oX4rnLO))5<%% zsf5MWdn$a&;KdE08MV1Nv*2ddW=C$*V(@sT)B&f933!b@m`om}8HMOZVH5$2PV{PJ zs`C_^=WYR1uf;}EEaOnjHjxlbden+12RMS9HCF5zHm=fp^3Z&XDO}k<#ikabIY8cV zU*cL1pwj@5afrQ4aHGv~Ze5a`XoN(9k!no_Pv$7jb4l89keCAwmZ>5!RP^09IwmU| z1HqYN1d;GZh8;#DLizl8GJh89!Zd$zLN6b)I#Y@n+%Wh z?>x!-{w04N{wK5o&QX8wI14r1y`xAGwMW7-%C8TwPJWLBk7IrQ?0K22aW3EIbub!- zHxcqWg!NY40N}cEPSJp1!i5kN8WiEtT&_ijJDYY;0-pP-pPt4>Ag&z&WS$pUBO{Q- z)JH|_jpQ1>m*>LgZzY+LeUmnsjEZd>-aemC`%b>=OEOf*?d+L-^U8g2YYZ|DgdD{+4QY;*n9;0zL-9px=|>}(ctRWk^~Gmt0uW>0xp=40&@At3 z-IgLxq!FQ%gxt6`9fIc?iIdWUuo906@dSExq*-nnS&sZsn2Y7V}`dUn-~a5J>P}CyG+`}XWXp=VW(pTnhHku8g15oP@L<67$g`2b& znlwEuLD{GW#|n*8dp8$j3;Q>vA2=k-iquq|kA9HcCa4$zUeCjWoAI};+f8u1jt+H( zik;_R+X?*DIZyj`m!m8G!|E@b_r)Zb?s)`E&eMG3?`b!h$#w82f4(+vU#9;|X^j)^ z5bdhZlVk#7Ki4uXyy00J*BhYm0Ad0Ec{qJRS)gzx$VaC+9&a5 z%fs=6qCap$5(N10Va*pB79>U{RCuNo4vrCI2?BV0A(jM>pKp=@Z1uj)7JpylpWpKd z{!#uriB_XRMaElXyEGA0onO{^pZt_LlRy6WlU^Uh6A>)r+6H~5sU~7vHx>QMwt^rS zScG4n?@7yuKevYOA=gWsLUBu8F0qlOg@B50vpZhBu0o3x3MicLfd{621Y*ua3 z?Q{Sr4>9Su^d_6WPck>O_K|L;s3)VEZZ_FR~b zdy_u$1t?ipHB~VtMzzv=?L0CQ#nvJuyMnkn#+9HCaGk9I@Vp2PvSHx&p!^W?MbuYad7uF{m2Siq3(HNXDHm$= zb)#Vu;)g%}^n>QjT+{PRmjKD2v<1yG%=KWX*&ur;p`=l3x1ky9Ztu2n$CSfR+@|G3 zWh)(E!t!pK8hQZO|4uE$nau7nAc$z-^6kpr zAp*FG0GN|R48J(KUZ~3=$`^{Xt@z+d3+TGA-rk+<-;_2PXETNXu)s!sSyBTg&F)if^)#! z=>EUR1u6f{6#AXF-_h$#a3qitjQKsL#w%B*CwaQAwx2pG<+6u*bLt>HE>!Q~x@ZD0 z)jo1jK9UT?0r?`pGigxudxaXd>h+V8&lzT)FFU zh-Dx-qQMgI_&eUfQSPIo1h}r>efOyr?ysDS$TZ^1+(aNbQ|*8|2DN93#kw=W;y!uo z6MhmUrspIuFLTlZ6@cS-{MhsWP~7j@Y1)0m@gQgj@@2iZ~J!&0+bGgGmwTsb5uWiMFC!#Wh*qS#ZenWC3R2^LDQtU9gYCRFmY=; zn&BaE7t}AKBS@iTG@i?mN8HOp2gGv`^WzxU|MOfVKdWqrMcM(AHomWI=tPmsc@XQE zpofcq(e%u%hgcgThoB%V7K-X0Jk3Aj=d1@BHxC?!Zz#uPwv4M@!*b$=n9^@G)a;cu z-v99Z+Ayuroa~;`GVkgyN34zOc_{C@L&TWdK!&|;CH-d1Qc?a~*nhJ{;%(ZI;?|Ow z)YX!l%&ld%Uvt{Mw_Sg&n{^F|kmZ3cmJEwd%D7_S1oKo(voY-}K_!O}`Rm{9vK{xa zp;+&}@zDT?7nBtztL}s+jbH1{HB1|jDdU5A) z>4uBKw;SJ=3c{V~1ebjg=qdgQl&m0XxlDga5^TEtt^SP9S%AJ8(O)>6eN(tEgzLgP z@4THS#GmSM8)xsg=CjYP6e7qpoQe0F=Wpu0688iFUd1aHX{T3Is!>nv2*#9nq5q+NqDg3gV)F8fCM5O77zzyI)R1Kne+QdRycMmnlE*oDSj#7&lHMySK)wM zdn=CDMJ1oa3zd8lB&Y{J^dUy_3c30A4kD6uLyeW$U7(GNB4br`hXYV6uJaDzooDVbuN2~b z9bWQB4ySj$AEjBPMJbv)o_;sD7dJh(+r!<4EGh51O88Ev_P!yS8oc*G$OX!4C^xCK zm4b$)x-!T8yvoG!$a9uQBd#Xiln9z0#Xqr)2EYp~`Y+1#o^=nXzUusV&KR09>No`I z@ZLR*16Hg*Eh4*5_0JcI4pV)iY7kUt576`SHprcr+(T=-qYgWNxY!;|0E;9(eW!dK7&s9R z713B}T^SQ@@p|6OGwkyA?`s}fNcJ}LSLDw*tv<#-?Y`Z;Xf)(Rj10q!rIl+OwBe9J z8t9Nw%;p}_^s%Sm;~Vb-rs%R(1#*Gqdjo)%C@# z-)iE;Hh#?817Q(jk%jJ3C6ghp#QAX@xF14k8y@8oMUOnwiH535YvPzn!_H^~_jL{U zZ`P2#gwP|uhg$*A$Pfb62E;79pUgr!O(yyKHeL^Inzwjt)gt<_@^6YnDej$75qHSe`I7%<4O*cz{{p0_4W=#*IkDf1+jqQ zk@!uMQK!k?z@~wUqWx*)7`Y)p)3k$pj7>_~Z3BbF9(e zrlCO|bm9mq-PfWWgREuU5gZO30nVEk2Gsn>QFjb-RMSa^D|wAhD&Bi->kA}487dC6 zB!daGU?MNznEr4pK#nbheM4BVG=WoVO$Zv11^_Yd=bukCmg)rmkEdF|Q=~u0ISNim zQr0tsVHbM-6zy;3YfWRFxIykjp2&44KGB#br&(%e!{QM4M-hYFIP>*k}~zra5J_xFdu9H^YL_y)MH9%0~L8 z%;Wadvvtbnx#ft+9ui$L`?a@^JHZq(V_#H+$n7BnHktr3YKAvP4^LCN2|=Z5QB$d0 zUm3S{NkdGE-Z7-H5WXS7K5Wg`*I;wh`1KBSc zyk1&&KL?}tvhq9oP8Ih)Z8UU-XW3nwMGMBK#~E%Wjr-%C6zFvnVRkGJv_PXX%@+_s z*5Jhp`#lne^=e(&DNXcUWairV2WwFyQ!YMNXgb&T7xxQ>_l{@~#s5bif7CpF_1LWM zI*Pezw52UmouUfOVu8OYniFwMK=zotkcg3Fs3qUiXYTL&Oegv@08U)L;7t`4x?kKD zSocSdAGv3BQ`DF_FF5&MXCwd<4HXj1`+lHG$>;VAA`*GB((A*$@!KcnfqZ%Sg&qfp z1lF12nORb90$4-sPT^h#l0az!d0pOn?|t2t`8dB)2&Z^M&TOmh7z&{@>SViY%wD6+ zbmQ^p;cb)TzzNZKK5?tX|JX=FZm=@9TKw+^1-MD^Klmn+Lo+|5$JI=;GK<5`%yyy2 z%yao}izL_QNsCr--7QL)D2LL0$!Cs3RIj1{c#(S>6r5c%hPwEpfg6a(#svjcqbKJG z=g_YFoH~v;J$D;L#5vW@?2gUy_{dfIqkt#JipD^{QMmHh%>PUb*7Ra%_C9r}JqQ2_ zcbd}iuCFKJWnJ#vS!m7zm4HGY zgqn#|n~gEZHsVyc>Rbh#GpHcP_Fd>8fhVmsj%GNOG?l}IHk8osxwP00DuMike1Uzi zcG37CPe_$`<>ACt4}%Gt8y^l0hri9@|3>dHxqW&-N;`l^84V=! zUHoLO2Ht=QV%qqx)U^Q|vr5}8@qgpHVKkIVPc=|x!Y$_h?2y5@-){-d+|q;0s%HZz z^~AK-b4;Zim(doLb*q_L9{Ef9#G&|Sod!VosH$a5ezBgMY(mIo;M&TJCI^%FQ>Org z?VTYGtmq9F0JRTFQP2!<5VvsM`__CR0(h_H>G|A$HVW?kWeLklze~ttpr^yQ2jm?8R|%<@zlHL)*wkb#}u5#;IPwxKn(9+ z|L1pAu?GT4@yz}5%{O1`v69#XuMk*&SRL{+3TN(vfGV0EL+0~m6v1boeP&9;`R5;H z`$2UDI5uBn_RqcX&ZBplPhWoO3C8bv+VM;C!;e4c?@;`In8p4m@}p4}Jka3ZlYD%T zx%}+kXdX{>T5{y~1fQcR6%Bw8WwI&0tpQpE4_wJP)S!RK#{so~MjiG8zw@?Rf}XS@g!Q#d>DHl<@)~)7&1(ndr~ifL)f_-& zU;Z3Dh&@yH{hKDTMHl(cwS|N@xzk7^WPrS>s`-Ko*}ibZV7z-GpE>e^FQ(LkMNN_& z1%@|=4TIIcjM)Gd>8qk~1ga}20I@lHCtfcO#otHCsw3I$DJW^GlkvX#RBg_W2w2rFa2i zr%_;qY?Cgge^`tG2aEsIZ0L)WV;u5JNHT6?F7p<=rx!1&Tlh=YnlFUkjw(3rg+tXT z#6w&s)uB>0ZAH?c%JRC1@f{ttc6WExYUaEM-$cV98UXyA7wsS~e=yNYn@YKr1_1k2 z8^Bx`pKH;ZG=TvA&-Z1ef}V;UpX{!9@6>~ptV#obLXashE&xjv=F7!xE@}z*1P{23 zmb4t{z5}Y5B#ll^!ZJ$*c%(&W5Omeb=iIG)@}IHB%D=y`>q&yZ@jQI^h9*wzk50eo z1?1qFn+F^##ip(5x)Irp79oQVp0q+74x8sXpSs#F(D$dxF|_38mkeX4*yNZ4)d0uS z0&*2lw<)Of0)RA|O3h`8;SCd>hd17QqxmF@0+Li*>N9bXX$oJe(m$Mjnidb;7LU)< z47i)u{m9{%KpfxCP<~+ZT=#Fk{YIhq&>bRqx}z3bJI)+xmcT{k8(A7-rrOE8aG3q}{> zI*(4r|hn;`lHer>X21}adt9zqbRR%-q0rY3Kj$Ep>9(ZR@L*n~a zN@maj89dfOPC_pmNSpW61Z`n+h&>H^kW`xmZwuzq1A(Q{^AH(TI~r7cPyO&c zI|Y@Qx&U%Ojlbmc1O|5I!^X5Z|BrVO-t;e=cWp5BRVk>06R$Rai<+}N>|~QB9^%1! zKJJUDh3H-2)OTX?qaemY+cJ-M6Qng47~`pfCR~yNr#8Rc$V|$xL-d@@Rp3lx*3rTm zvzMM*75AkIxm75k{;lXdYz6as`o22eoa&%~_diYS7fuNyPNZdfAr;M)M9(3b8pJai z35q(*!yodki;I^!{GfoR37}{)a-gt?b;0W74FFJiur}uJ}dgdn0c+4A&BQu!(X@drk23id_)e%aqw%&TT20(2ypl38fdZ#)2=XzDa(mL1G(Bm+{EWXF*j^f!ULT|eYIM5#ZQVZ zk8Dwml%kLt5M}m5K<4EfVrne~;G&}eV9kP13QwH|#%6LUojWvM^0fv4+-zRX{27qu z7b+C<(lh|pQuehW@H6YC?((S%SNrT#JHiyO1Vy-M6h)jrT>P+LOI6`h>n2PQG5dl; zwq8r!Ck5cchY#Gc&Y*GZ7k_uYDE^t!a{dG;pxPmB2M?a(rP$`!R@aK3+?1Z!LmQxm zGMccTZIeWUX*j?7o7(p@%JBamnpTOMJ|;~Ps8M!TnN)hA+cE=nVK^V0CNU7b1wXQdY=WY=F2a?QWU^S z?E_Dh4)7@N%%z4tnn zO$V6c?Z&8(IXYE?u0kECxoQC9cgc&k0nkA-YV+Es|7lUhbLm+9wbH`drJOe=|Mllm z>JCT-A(yEuBap=T-sdAvAnAVgfXp?+$PN4&>r2oQUK?Sq(r z2Tx2`F!15HJwL0do=5JAbNA8;+7pit*G^}9;R1WQ?q^NMg0WCbU5i0!Zk~s*Eed90 z7>xu_VXD-hkCj5X@qDm@-i+fxw&jm#m(-B1yuuaLj-?07p1o4_)knECL^{>PGE(^S! zDeY_%_P@%YcWnUoww;*?m^?7u{uMP3d4n>IVb`tN;xjN z@~rt2*Cn9HWU8VZ^SgJnK@iZt)%98fUYe=WJ~kOkg;vlM;m1%7rs05kK1l;Ybxf;) zz6V8u<3wh=PATG#oZHTNGFhil zvT%yaaW3lLNXfbScQg@*rmju!pQybb=Lm-$s6SJ7k_RpN7mFguN%FwYSr@hQ%WJ{9 zKqJ9c&?f$NoV7av-^Z2k5$J=jzy3y(IvN0QC)q!RJYr8k6$=Fdbc_msnmRLU9up$A zLlcQ}0NH|LLWqWjlCwfL)!X{mpYanHXfp9G+&2Q=TH6nHguZUV4L^ib!2eQs{Gns# zO%(al&fr9`xpg4m{QqYe_RlwKt&JDATqeIda~eYA)UD${^SVlc*I<(?dvgB&D)az( zeU^YT9dur&%h8{g>-4DOwE>!}(;^L10ETfOGhm!4lw_nIpVLOiL}c2ZvD-x-rh4_d zw6Zs|Ri>e3gE!X@{HzKfwd!}kjCaJMad;3sZ45JC~IUyo_OVw!OD2 z)I*R-%|vFH7ral@IU|gZ%#aZ z))i-4b_ zC-8;~0=Y0J0rK0r@-Eh~EibXiBL};=~E99mll8^3L~7Bs%uAG7B{@Xe7J^>RyX@ z$}u}!Zkt32NJ|AcjMlFlKv8h`j*)#VEoEvTD0|o>SLa#YDXDn9UtYG;=z0PT zq@F>;av3xLcY7(5&sxd9*F2UwXXzih>&^@qIrYWx+OdBchy6;M23yfxiK$+%3KCOf zJph}o%?K*XV{)BkpdIh2_$p2P;HncRh^2-cOJ(>HR6>1Z#hMZ!h)dI0wUb<=5LCgl zEQ-tI{f{?)FAeKB|8OX%b4+stxUYpVF$0@l3h1!@g)bkX$sGS$U16*dzwnrHd~CV| z6!_@cgxwxZ`QKFE!03&vxN7j^Tn{88`#lQ;g9{fT^0D&CI}*i8HKXS zO>GrncAH<;p0!phU1FXOD_W#nx^d4^Zn+VDnuI7Ty4mNkz&Y6kP@!1Efp#NqvO`87c8bEeGol3Svm;-4av0D-bP&@C0_Tcn~CK21AlD$$m8QQKT9gzT(zznR!xtHt3fRT5^B z5dQSM=sP^qCQY>NWS~(brRl+1p18QpJ>YB61aQG0V2oJD5KQQ_tKNc(u!Z2!SocT< zpq;fB`?=c;GF|@Ni$12v1Sg79ou=nvGCJ~Vakn(_JB}wBC)S`z1pzQwOw53mrJjJ{ z+V{icoQ^$70N4LKCovoYlLDv`cAZ}7`l+@=p_Zg^-1l~M!tS&uHRb+{#*~Twn6=%< znW7N)Ew>1?l^?URK74$5mci}gI)0e>_pGrN=I%W4Q=a5gKy#0Ooj=2U_eB;K)Cmh* z&|^a!gkmwr!vy{9T(~d1iN$e3g`uQ%}BN8`^S%Yz{^0~-kK0?r5kLw9PHBf z)5d#p`repfx3%rvJIZC0tzotAlop?Y8?ThX7ccd@{xVl1^L{VN?^QE>V1wNwk%8da zA1lsc6Jx#B>i`1aZFd#8UYU&}?mEBy_PdJbsg0vkB5eF;`OI|)^yY0qlY1*S zvlY$o+`gfdHL79sv^2xtdGuK03O2cccnnHKnn0NiLFFSKP*Cz}UG%ZYn}~hnt++1S zKfa&!fF5X`$G#0sx9A91PcggaI>_z@ZQs2JlszCwvh}?oJEIR~Q}7PlY(I!_><*tv z!;|K^bWJlx8O=^o5Sz2}&SY06YRQ^(U{{9h;g_SQ}LMUiO8nbe?1+a zi3~5=1J0R_Vwn9Au+$#r*>mR!2P7@+{BYMi`al4fLi>5KPBXimS4HoDV_3K_*}g=M zR$rmz(?xe!yo8x1dW*0R81L$e|rZWAwP;jIOYUV z5~bgA(MN~+QHGVPE#mo^D(7lZmV+wSn!wIAU`_5`T~`_osbd9s2;A_xD)bPQBjH%D z4Yv0ntLop4B<7V-6%g8qbBpFx>LFXw4WV`QG)LikYwOdwZ(g7KFU}Ma$cW(_HS1YHk!kI7cQd6<~~uYK^0Lt zRqiCH0~$6YQy>aH|NOt!0LW{|l>eSicowCYW}RpR{qvvy(*1t^-$kD0TjPS}56=S@ zOMyOEXwtkWt`Tg*jx9Ho9=&al+kBclryK{r!_PR^n$}~2hD(KQ;wepok9AcFME-a7 z9Zizu$x`>Z*2Hl=yV19={azZA*Vq``$C4q~?@H>2Oo>l>E>D&8w*1$WVb|7)yYEZnB`q*xo*Vt46A*V&49gXpg- zrRNF(P(Y?zQif{6yeLJcI~*uY>RN{s9idz4duMkwha%U30>Ds5dG+~Ax%zGojG%2x z>s$<=7Ai)Ai|`R|4V7l-Jdg!U?|F}dUORV`fQbg1kTk_|naX=%Zqs{5jpgG|Bw5}D zhyrzT$oZG*L{*KARn^!*Uk4Ncv6uZD4e!@-?l0V7NreoRk8cB?pv6sc@CWm_mR6E9 zrdhrS(bKT>rG84PdM$qYC#$0Ab>A60?Kluk_*oEN%viDv*l(q@$^?!y}a>&Urk6ja9!i&t*^ix>KW4`1Kt zuD6}kd0wg!6oT7K5Yqx&1!qo*;31O)6)5ut0NvbHsW^4^n>wbkGI1MrIer)s9ea}{ zY~sTDkRpy?Mq!Rd0R?0P=HUOykdkPPQ0Q3f*}pSYR{z`_bP2RfjcGLedj3Q^?yMJ6 zTea44C3mI{tUt#?cl+eD^{@>HW=HKirTt8krQ3+A6B?ZsmX-3+@49{5o6j zaeYt^29B@AYF^9aXcdK*uchkP@8nC>5e2O8p@0>tu%FvV2AzV21O>cKHLk9dmv0lO zr8ab8ZMo)mv#tWi&AF)7fG0>4(3~U1{X^Gm;O`_$3x{F?p;sHDd>rp(Isn^&J^(EM zR{?|tL3Tv5DTFR98MA2*JWCJ&yjgj^&;q~~5WYV@LxAyv4?Z-6g$qi?eHhk+9Mk*R z5rD~5C^+K?5bK2PV7y0y7NH0Q)fg~Z!mqmDpLNYhri@~7zX%bM2#Q=6+oiN)Q&%RG z)tlYLlAGplQMc|0Wxp-h{~o`$7r0XPY|b96Fiv|H|C>6t`*3J)0?++3e(z@LHZgTy z`xN)eA>ZoW(}Tz_)Kd3Y8>%O&Ke8bBqakLG`35>R}9Z+y>A!mK3R$i&XMAV!IAjdhSZnL0m~|Q&`a_LFcKi0J)hD zF6U%Dg!r>mxtq9cux>*I{?>X7 z*y57E@cZT}(-g7ka3P3qb+t+Dx{o(dQ<;LdYjRX01Xf*p zMeqYy&PX7nZ~2ufd2s2Y?|Yd*1qNlFV(*QMWVs8ImkJ*MbpqRezO^aV`l@JeA^I!(yuxm49w=X9jq zrfI3?gNe|~> z-1*1akl+j0{5M&|?w4%&^E*@Id>lZ^L(uVhWs?#TM4euDc*N43Ym8#;(f}*D5~p({ z3Js#W8PZO`|MZhjH327vi6a2QR&nSVUOPRHxJPp~@)m3MS`(Qj0M{Xk{TflA+wk?F zF#t8~K`xGoXEevzE^&h*lPnm(+`Nz;r4_5AF*F`bhIhblp#+4oxc^o zyJMZ&ms_dhR~juJQa3G{v?FbIunlT{lfwUyO#_kJ6+M*!2ct1b{-&o@BlU$!ssD&? z2`$o$z6^RAbuu5}CA}s<>Ta_SUFwHa*$WFHeV&(?NncWN7N|tK*Z^)CFPAgf=B$`* zg5^lP6_Vl2ZAkaX1Pe|i-5j2`xg^q4=2z~q3iF*<6Tlk)tx(;iLa^J!e6x6KZ(^=f$H`OR*9~k=o;R`0$ko&&Ua;^8OZuj%X)28JGFYz0nC`;U%jb z_gtuOj_eaynn9k|etwJjFkfjl3r#T&d9zqsAjJ|eL+&YhVmccQ02QmjrNFs%;ze=Y zG3wY4$tD!JhPkQmPTtoWFW%7ern$h{@@2L)ziVN}+9NVYVajLV{&75CX1{y5mZ;dP zpY8FfUI*4IXa?hOx->blYhQ?|{H{(ryl7;CK~o}78o%a(_1ka0RoFV*LVgCEp8ILB zTy&C{Nu2_bou{ca9l)AWYfUV;h97_Oac-o%R6ZleNfFJI1|uofsE(|F+8L?QptM<1GQ$f;XcITA%tFZnz`p|BpPM{~Xu{n#imp&Baghum3q zqXz7d1f=c7|5ls79Yow}JF;CIN4D)RrNfIG4e_vVBY8{B0M%aP#>cy*Qr>&kB3rqz zyHhjf7XLEGdg?D){LkSr>NAdAJ^z>gjj6LCz@BN*7>+FcFGWq|_;IR&vM0rQSXMDf zPt%yr!RymzkSpcrU)k~G__+20F|>+`>hr|=tlsv*DGSG5up@4`cK*C>KM(*uTuEur z#;X(+;Zq;E9KU|80>f*~;}p#q`P!l$;&eUY*Og8bG3c)6PT~4uu1)5^5P3?+Dw)7*`Iv-kBQ}~!! zr9lX7U-f@chE?eMkasXZfwG`FRe2DxUK^?nn?vYtW5MhftLr-{d_1fUrHuO@Q}vF-HLXFi(^b)L}hn4)7g8jGr4f{7m=ua{aOd z*ART$$^K=7Z{HOE`ABw1U=$Id$NW4a01Ot77@)3K4#2FLO%v{vhSIw7qu+_|H{*E; zMP`MO=KtpSD27$A>B8(v*X04FPt;?49EhKxZprGs-0j$ zQpbKxNd&oP*=9_Ug7y;#g>v1}NU)uY1Q2;`yv1gZ;!kefaPKzG@pZyF<Nus7u6B_l%9rn{BJfb=A98aLO<>v`LsAh03+X#5Z~LOIg={!Yc;JmoDjO_=&k}Dhr;D1j*aah2uNdx z7!mF8`}zdWoQI6k+o=dJHVicXkH;RgcPM!P>m*1+loitJmv5GOoLGumQ5e6(i z?5W7M;(y0pJ?+umHY019M9S5~ndMZ!cGL7rD){?=Xqm`R%VY!Z-M51CvA(#-OPT{V z+=94BqXeW;$VVEU0?2O3=h}bpIPQvg!gud3BjjMK$^Vf!$Vm}d$PD+qwpg_iXZ2c4&->DuEQd6VPBMPbwi$D05o z68s)T)H9tdJo731sY1;bDb0IH%ZAy(ATJ;28%5c>?>^S9@^g0ppm?Ht{5UJM2WKui zf>QZ=$rz?5#rd`tc4m8cuE9{1<#XT+!9%#~Sh}(Q2MWP{ufPs<5}n2ASQ~;DKhr6| z*_l4HF#(`xObc7zSyQ#92k{+*9Xf~469&FeEe2lqBtzYS-*ex*P{{rFCT>S_!i62J zAGP)=$GUBfq*SOdsn>d+iwsieXH0h7Wr0SGs4p;3&1=2SD8SKAu)=!n&9f{2jEPWk z++6Q}eEJWCW||hor6K`X_lt}Fr~6Ub%H4Y^zN9FOoQMJK*YnLu zfOWwg;I29eSmH~QBCPmmzOhbh;~%*Jp-D{M#}D6|4nf#aa(tm*z_sUdq3F*sJ$!hf z*Tp=RPqmomzWMp5pPL`v`a#c|W7dg{+kUEeW49thw#xbkqrf4hCWHn*FbA87ecl|=wkVbVG;w);rVYA$d>#}yI zgN54oRr*Oe2GNM&{9mf*-bF!srA6(6V2Do};Mh5~Mf)8`0URq;jBS{v!1Ej%URuK# z7aJNtNs1?=HX6LBChpvKN&zS`nk8HtkRdN#K5w0!R@Ysq{DyT_h$*&+2XiGdN*d=$ zPD;)MLhq;S`osB#UJs3OngB*&8@SRn#QTOXu&pjF(xEkP=36p0nU?O_Y;kaDbGbB% zSD)J!9dmaZbh^1!?SUO)=ft&1`#LsE6dBznK$>o#wpK=IJ^^ni=dDrmGG(9BEnlaD zyR@!-eKt&a&Nqto67d$Oj?}eB|7l3m3X8H+>>j!W zZH?E32gZv_q4;;LD7HzXahT}vs^tn$uN*w0iy6VA>phjaH}L?vLnrbcj9UtTP2(F^ ztEG?tDr{n4>vmr|cHyLW%r9D4AW$}7Xj!S%I<_G^W-ocZLlAFyx-1=K>7@=+FVtEb zFD#4V<{*Fq&t}@r1>MxxbyDa-7jB(tJg>L5$>+AorF+TYLOaulIV zzcum8=R`2R_QdeL+#mp1LBYv-Un^w2qDDzGGINzJc2zy($-14g9J$YQ)4bJrmiVTS zXF|>~Qv5gGKw*l)XN?-L$dmYYitQ806!+gZ-+ZIb0fK@g7HT7ykpvtE%?_@mp)o{Z z_*_HuB>4I@qawJM&;y7j3ie9~L5kBX#L){MfBcEr{y|ZAX?BFb8#xz_@11wv*0qnR zd_8@k0l~F+{0O1OKU9~2Mg~a|%{oqYtbtn z>p+Iy@yWe4bMy8tu5B<>nhNWUfkd4T(Tq7h5seQt0gM)Qrij8Tr^Au^Y2KkRPRbx; zGb%d2u(66NPh2W^E)>v zXi!6s`!>gBrLN(6^X8jc{J)z;afzpVu@2U3s+-ekR<*E;{hrLEKc{})zxDZ-9eGT$ z2PI%$oZ6>|ms=JmoMRm*H^?bsh&qM@7<`*{8YtC-A*BJJ^md=PdVIS9Js53dr=G%h z&v|wr&rXx*fHg(mXagYK)PZ4(reLMdC+Uz#8If7UoV^_enkl2hir*F8;ze&V9u0ut zUQ?uoC?D`Kx}vo*__pD=3}_D&u zX1iO74^pJHs|#-t}t*%cW77pmF=Hz&Nt`>pc5$2_&tUC<;G7SBPcSDe5$;04~QJZ z7bS}S5TFeEJ&+Mhu;IT`Aph{g_s#cN7!d9szso(u{R`xNA&VmVlTx#XJot8tN4+;> z;p)ccdhJNGLO&xW8Z=tq=JT_d{4=Qt7X$7=MD<8)n10ZJQHB0o$Uk+3ea=g#3;xWg zC-b?T@4I${VF#fxEIhP zcr$-q;iOIvT6iPier}>s$dkN*#!wW3qaHv+(2T)aFbe(ab!%yez#;eKQ-^M{PeX02 zbix$p95j?bhRk{g9H1fIp%{H0riG|GBiZ|D-PJAF zRjSGnZ_&6|gq2`l9BX(sxEc4uVmZIBP19$ozqfYng7%%-Rj5xDJHF5qJk}fX>)fea z<}6_1$rsvK2)pQYmG|^Dtm_7{S|XpD8IYuJrx~AE+2F zO=}t%8e1=>C)j`5TjYOfnEGEuQ|Js)_h6uj>~|BSaXCEM_Z5DVTXNt$mV4ag^Q*MO zQ2u6K7fGY)9CGLfTgE(fY^tpSCd&e~wu0bZX#iXWAKwddUiN2h*{20yLw9!FVN{IE zjkYcTfoNMCEfuP!9B1b@Yttd`2hp^EZ&~tR2^UJ&?s0}mLh1fbXbo)*#r^ADHuk z>Rb=+;4?wy`VzP}zv5%>d`gNk;Pn)B6n=cO{`?++8MSx(ehs4Cbh!WP+Cpf|?{;nz zzzg=Zb1fU^_M0f(9i1YWSl%}Z1?AX5HoTLM*OcZ@-0L}YyZs`2zEvyr=6;8I9elwf zeQp%8h_Z3Mmndv?GXKiB`AS#t=2K5C;o~xT^5phaVMDrl-Xl`8WMH?l)aK#Q*Yo!ttkx1ML4;3;Lz59hv~) z(X2FrEVu}AKqn`k8jS$G4xU$%HIAR>=l%CT@FYE{h5-o@&k>2+J6RM-)a~P_2wt*1 z5+?Rd5=euCMh*yq(BA-65SjhW?U&3rQ?*`d>( zW^g?V+{pu1Qht|N&Up%1#;sisvc_9O*A zWTvq|)}o7knS(60e?MmUH7d3b-B#}r0&#gl8*2_U4=6hurdYhMlXkmDfaR|P;gGhZ z|7nZkO~sSrKCuox4i4s?aO%^7I}W4QMo})Vf5{V}pP^9XV^i^`IHqIiB(ssHV_nh#yoIPNQj^ylOJ_S^4#0n+qIAUkjv-d+yVa3;{pUe7VvcR7v2;#-#L+s_92Mb|FghP6opsT>QPgCPQ_v?vJo$RHDh&W@0_1mn&f{9jLh4%X`Nco-yiYX+@-|h|C+A$i z_Gw&4Lm-;#t=yB8=DvyJ8f_H8XlMW-9=K9Eog)`>PIv$Q3vD_m8p6e$*HTfA!^Tmy zBP{2MK@?4VnhR`;oZ{T=ec7K15UZ%|NkLVZj!{z@DYdBOF3;`U_vWS!)b8(%5h1nl zPUuYf?Vn%l&qQ>|4$aBQfuro)Pv6os7TL7OO@$=Tah)3&es6fd%ZLbN>VoxK8o?ll zr7Hd-h$h+A_#Z$Rh;-alE9-dOTsexx76icTJ8c{Qk!1gw(T=~F_EhMU{Zxl##NN2( zDOsX@RMc51?gF+;C-&9FOXX6T*ozf=i;gJYc%!HP`m7=8?ZUx*@yXH12e$fioXgA%w)BrJ!=*HwrciE#Q-^Erl0w z`iBo5Xn{xK#P3j8bG$my&AV-g*?X_FJg*$VEG_N-g@h8h5D;4qpLRS1SWruMg`~{CUQ^> z`1`9rOvgj@2=ez?5qr8;rE8K%O(64s-%+QBr1YZnJn4jwV-F$1X(xxnas>C93>*Yo zd0W`CU7G;1$C1l{Y~^0lSDKfoO-*0=Hj9ToL)j|Wv}-c4Z*$?0#eX+X*vjLS)+;+S zQYc!Xp@mydyj$z9fW05PSnMWUQTDRh3~}9nxF30L5D0_1QLyWxrQ&!{3Gzw5;V|bK zONWcAO#pAC@-7WdJB?VicFym6)d%P|0DAj37XZuCwBQZ*Bv5AzYhJ!Gil__7>Uv1` zogHf!`#Et(Cbq|L;_2F;|1_=<7_2Rz*OH0@gXNx$qCChyI1b|dgWOPNdz|OW^C^md znne$DJ{QQ<6!(Ly2Am2~iAxNiXuc!(_ftk{K6lK(b?O({2`LiVBJys*_b)(ebN_oEHiE>rk=8ehZKwoa7F1xg+J z!GL$MPgUJNJ=37gS16)iUR)G@*fK<1DpmJw`|X~y6@g4jId1kDD(Amub$O=T6k1Q7 zyrIPl#hpRumgSr~0XglU8Cvu67OAZDnRD|gvItmo=3lrF9EDJH=W9E{?O4^ya#3v+ zjrUD)&-nsbd+rE_3k`Blw9tDy7kQxpVD@iGkqaCb+;FDQG)Fil3Rb4hOchKWUMd7Y zx?vNonw8W#3f{O`dccQ!r{ZzB$R%0ODY@%beEiYJ+5n(9ZB%6LqP+%-5p~%ef@Dch z-KHs~Dsi3exIJLqIN}?gJ=Yid0{41oUeHkC_gUBbI;}s|DF*i>4G#55erDo;fy~h& z+9h-)upiGzDL|Hh{)${kh zsqV+5`K7)iR;%lcka6VHKHf_$O>t9bc3iM%So~F*wuv7Fad6o|e%XH70(DFyJdKHk z6}_6Y!Rc?eIio`JiC=OJaG$j{ z&+BT`DAd_In3=x$V(K*s&(P(M6x6Rw8%cZ`-Xsf>Y(Kd8 zd|Sb0MVwB=Uzzg&K}IK{RgswnZ@uy4#Sj4N;$2U`cq2pNDTbCyE6ms@d~AZowR^C$ z-&%O41b7*qD9MCKV1W^?gL-kAJJ-g`Cm``L>}&`;VYyQT_k6t#8b74;8f}(#EkZSy z&(JOB+}_NcYp4b^J2=l@b6}(BMC68qfC=s!civP!J3;WPT>R4z`99mkQIxS>AAj<( zq6kU=^fgJ6Hti65PehXW@A0H6pi&Cn5QJpC{`Jp)s*vNCIY5VaokBv_qUQ-6Ag0~{ zAyv43I%-myIUk##F@@7gS}e+R0<~a@Y?2U;F{TaTjvExc-)FQ1u=oG2*2Sy1Q7gW4 z^%6e0d!qLQNyew2d}@Bz`8r&Dd7%pZUwE>ya1DVd=0nI6PCoZiBr+s=6!EMP$MxX; zLlyNqa}|Hk1ZWDJY2(lA{8&rAc`=|gJ z>I7Gi*xSl0`V^Dxa8KF8bY3Q?gqxr3IG2iy{%mLXqR1@Z_85FsUT2~!J;~Noo9xJJ zW$V;gH0i`v8Fl}aaaw`MzS0Y$|0r7^{OKk=2iX%1BQt6XX?nc;rf{&6qZHM<9!@vT zK$(O6;-$Ig)L#p?m{Vc7J5dOplxAr^W{)Y~k2=73kq0W!EPazU8w_pG7S`4jmM&E> zjiB~>&q%86)Iu!Tse`o*aA){8G(PizNEH9aPKP-5p^gc`uJv78(1DBAr@j>H;x@l7 zCVb-SdB?3<(X-KEjl1xWtEn5n)nzANQTGLD#S;y6YRJzP$!AnH6&1X+>wV#apOqKt z8-JYRZW4Xswsdjz?*MG(e~WdOh!H3=E;>8W~bIsXHzJ1GJ-UmUpuSYX_9oN8jetjVWr2G-!l zsJy*~tozpNJF2S6VHzCeb+O659}8A8&z5h~>Ao3LB$2VBOg(3U+Oo@&VA(vV4KBQN z{^}3iJlp`;-f~>7ud_xqmfw%NZ;>ElW|Xr~^*tDf%qjl2hP@q)VyB-yxh3s>eG0jUgh^!gCPR6+Yz+I`R6?Of=n zO1fI9J+}48ffi49f)33AY}l+Za#A37*K*x`(RSYAJ$*?HYIgnJ$t*_chVY){CCMiZ zSB0{2FP{hBXd19i@aqxF19DCwi=ZBp=+Fvi3�Ss4;uT`dmUaxQIPEG@itO<09!u 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

    bIe5z}ePhXw*_DM#3SD=|eE(2~ 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
    P(seOY+AUD7d7vavaxvX@jr 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 !GrOQ#pr-ICEv%ihxbIwx{ zVxcNcjj$vQcg@#0&JYn6{*DMgFj8c*jHpgH_?tw<{V-&1p)(!E_uadmgwaO#X{Y~8 zeJuhgg5x;4eG6(VTYD{=@=Obel!6V8?K&wUx~~SPyC2k3BdiaIhx4p;clO8Ha%x@! z5)~J;@91}6Zd^7M_5ORf{qrs+fXfujaMFDzuCFiTxN*EF06?|?P>Hb) zO})fP{`8|rwp=dY&x~di51|=NbJzQ+Ngcv|3-=oA9Erb#;cn>uXA6%q*gK}N^hK#Ll6EfW3Ciy05|2+F% z@@`YfkyYljiIA~5BI~B7`AfAg+>X~8*Zbuy$23Yy^VhEG)Dq@da+ckj02*UnSx7}2 znwKj{yPqgr(k*)L9`jK5pDQkpw_F5Zp1+QAl4rd$uxJNp;M+0$t5O5|4tHdv)ZsRM z2Be*S%{}pYkt>u_kX5_Cl+U071H1w}plgSXKEUvs&@n)~-e-#x!)70!0f1rng^j+L z8HLT(19Q!H;ERqq+{C>Ipjzj3n+I?C;qHdrb;fV1&YCfGu+)qvuk+|MIX`o2N9Ri- z-Hvd+ceG1?_f#SPg|-_uXYZfA7r^DwY1pg)cwJf?9yT)9*5Sc*ScPkZb3`Wx?}=i= zsd+45V5 zx>=*Zf5xKZopX_5APZ+PZD~QE>sHRas<7&JIDZfBDJyeQ?eOPqrvQ*euep|&Vw4~y zl<72T(Pa~@Plcr)tT#4Jt_3h%Sezp0`j?mL%8$Ylq$U#Ccm7$5N)GZRRxhR}T+936 zcVSL>yr)N1Vr)4pMdzUAcss@aPBP9Uo=gCOrb6(7v#zRuLPS7bT#Hs0$0T&!Pbxcb z<3TBsfh7c;f0)ZSru*ixfnxyJ$6R`BCYlc(Jc=?Ir59^)!GUBSC3`OOtaE?4e}Cr% znN+IbAMPiDd*s)`{TFcVI2UyK5$Wf&e*nDu7Ndsu@brWfoTVM}UC+;bY5!jQRgk)f zatk*wg!JqAZ@Md}PM&)T=v`7^^1+ooqVyRV-+ss4Rw8xF!-&3d!BMHWR$KgTmyQ?HOd8Ef@Cu+P3Dl)dCam$X=hh&*IzhjEc|3UhlS{ zxV({!nmwx&Qw;ieq&D^-0DVrb-fT%ju}Pw#Lm0ERWKEo!)}X@qWg66H(7ekuel$$4 zZt1JH8Uq+Uugdr0WY$r!$LCRQzPVM;Ah51fOE_*7mz-4Gd_$K{!o<3Ax}u&V`HV3pkw3yONe^tESCP9P61N?y*d_6UZ06Io+ z_ttFnRWuiqW89dg)4*($3UJN!m`XuTrOTlDOYV3wQS4#@XGr(Qu8D6h5g~Yw?pZXM z>P7|Z2dCz^VUdJ{^Jw~o&8C0=V+eTUSB%pO8$GFWXWdbL^0RHDuZt_Ve z@}k@Sq?=$FPvPHRjM9%W=V=>khG!DZC>{QLGLA52gxVj*jmGxfld%NHb1y()Oh*Gy zd<5VJ76OjRjWT^*@XYcuZWG51?J}Z40kkmR0eZzRBOL2Rm=kzE6siFHp9t0ehe&-# z5dh;FV?7#XuACyhF|*KZqz^95h_3GC&?BE3E3@j8%rtpoMP`1PtSzE74|D#{kec#G zSOZb`03!(Z1O*N^PSbg=#Xns4O_cg@R$$2EO%q|%p1`l#c`eNDoc8aG1{8PE`3I0j z0t|`-PoF(){^x)G+5GbBuj1Q)!i{$RFAMoXLHp-<@F>fn9*WfqFNom(FHN#Rq7DA- zas_9c;5h_F491nmkER_e+Ff?(H)7|~xYXM_%B+zXMM+Y4AuJITj_?NzF=5%EKxDEs z6r%O`C}Y9Qj~Z^j?{f|FRDDD~YT+uZJ$PS?Q}|P$7y)Ju)(tO+ZT((SQ%hLge{p3_2-NdSZYb$IgTcD?02W@vO%0&3Roj;dMN{ zsL+@8!=v@=l;fX>xG@-41|Qe9w)*03xLO88*nG?K5eBZ!l7sz^*Ry(DSSh zaHgC4rdbyczV^>56ex}o-65M!J9cjYc-;6bTn{OVfC6l8=Pr{FhxFhvfd6prVx$JS zg>`HtW)L7wI-#0(?Ak|ARV9w?zOn_t6iA)NOU>U4w*JkR@F|qyAzuequHhq@gBT+k)zz|WK%vF!J+oUR2fXdXX|A`N0GDkR+2+9C0Tw#^FJAs7QK6vv zyE*74p?+gL3)MdWe)xgF)gJ&1fC9&Se0&_|wy!(aQiNM~9`;i@|IGo@UrJE!-~(%0 zOcM$BE2bQTVhHXZ&K(Z_=rZHH1wb9GGHoS(mMgC8{m($|jIF8XT>d^kHXyQuAd=R5>k_}=K4MK0zg}|qrD&0>Gkz7etE4iohYV2 z0)Xcy?#W45?=erjFd^Oc)B|FV{fZZOot?Byj%U5Tkn!t2HyFDjSwZ(dtRXlijAK08 z!BFt)v`ywE&7c>{y|eEB_4-QIIX?y|0EA<}dyBy?o`Vn0q>&)38QUYIatOKsxA<8) z{6kUR_hGN{O+@xhVXyOzHs25aCO1eH;H2rm6_#p#eRW>FH*j%i! zo*h-0RHDs)E5IIh*6gbid;bx5I#xGLbLscBBspoGbR`1*Sb67#v zK@uYQrb`gRvnZvDx76?^#-mBs&~(MvaNP?JZ-9$CmJf~%l_UoQAfm=b?<3I1109k9 zJlwrQj(-Cr%ZBXX1KCXCJyr=IKEl73tu{V25Zr7nGk(jNGj~cU*L%Yqa}}>drWE?r z0XW^rrrGfBS!C7-jY=NAY=rWS z_khkDG6cB>r^*n3{t=s?=*|ww0L9-`#?@@ovwtp!vyR>a$LAMY@Njd?=#DY{NVduByjwvJV9b&6A zl25b4H`YyDo2C_j>e@U0;r>d3#KjlWxqeZf2{HBiBKlXM|6g8-oCJjeH{x1ar~f2M zLQbT64j+l=;ExY6^LuFBbuHN8=fuI^qx#l5?%Hk(N4ZVn^Ip$B)NHl;(fEy`J0DOt@+9DPf96)AT|0K$>cJF5G{B~$JIhBH#kAf6W z7TRIPeFhUl@0%usU_H}9-vZ5U1yL5`n4WJ66mP7ETo??NYk&i2 zF>wr?X;A)QVE8_)VK{blr_nu!Fy>W`B>je}V-DVKn2 zDNcQmV@Bs61sTjSj7Koee*N_yQjEboapT2|`cJgizO7`bwFL@Ice%XYDA+cgJpeD} z7p|9F+hF{Md4ghZJig7g+WDWf3Enl5f_?u(hJOJ~A0!m(k^i~9ew2ePK1@Qt-BWv*m?GNX2PhbvM~ zLC!qVZ(vUR-i_?Ox&N8lZ$^BGsKc=Vd2@?AV;hS+#btTaO&H_f(d~SvSr`3igL<|^ zT&AlK&G2C30i(l-x3=9UvP%F^Qe0&eG47U+hiT+o%_zv5fn41=cJidD$Mc`ZJfnDw z-Gfs&Xpm*gJla6As&q&I&Nu@Fssv=st?u!GjPMTA@hJ!WF2Fuax3ACkvE!sOE(bx- zyvo;T8!WE)7H}H>*0ed)p0_L;68-ne%&qWB7};U73sWc*000mWZ}dv&!S590gbn7O zfl?~Y`C-!m+N`*L2NnP}&EhnSj<=W%K2~@9ONwlDQ z2LM1q_l@tncZ1NpkB+0BTRsA~1Bf~?FbI0L14H?DI0ly)xSKi(2I}yB1jm)E7|<3r zqV;~vyytD<6_I@Q0*rK&w@okj@6bXEY1LG!?Jf#sr?3(A_=TP?COmy65z%yzImZ8>(zqz|(o?;^189I!NIIbTNB1B1AFlzs1iE>c=OHQ; zMGp3{VqmOZ#F)}8+Yw>s$DR~0+GD?x+26;I4C6xXaRm~3qWm2Q{0 zziY^Kc7(41;u(adzKqkhFa6X;C;!A?t%Gc!J#Q{)-z5Wx9h=xyv*edVSyA*R*H_;y?&Z>b|*QXgq# zg`f9)#kAtWuEN&zn?BE{f(KpM@z6(Cu-PP3c~+`EHWeD(byqgPKmDZiQFM{e;YTM4 z-I&*=6Lxof01W&aUk6AK(Es>}bk5Lu5W0NWIKdxc2m8G9oqZ%AH*)-_4+y*ys25(rwE|r!&Aj{k%9JMF2AzTpFvx%>~^UF#?8d z+tn|@JvUtsfnI~aQ89jd6aYAt&=vGlYKo0wtzb$8y?UEDyrYZ1vG^`T**Wt=?vu+v ztfQ}{eOlxJKz7!L1O7|k3oVkOgHYVBIOfU&D9LpR zYa0q8kOAQy;Qj#g17xC$i~Hw54)@;Aw=e2%zs?Hp{C8?Dn0BZ&x#U8?Kh6o7e}I3i z%Z(H@zLU=Rzy9C<>KF1~91F!7mn z==i`{)c@RTfTl2G_hcZCEj^s*PKP}e>M~C1KX%TrX8Js= ztRp<&-nOcQ{5442*5W`T#1`jbL3AuW)4b^kK1^!>+6S75P0z5ENa)$5&JuAeBOl388*<~HN(aj z8zXE!UWdW+51be?0Iyr&X8<1KH9g9F1soJF3{Vnv z&sRF@=6Wx$vA<`ayP?C+C@$|+B3t1$IUZ&k@xSnntm(p z94DY!j#oPTn=IWS?fBweEt*A~zmNi4udgGM!OsC>PGmY^L;m)yYL(Hs#MkhBTq7L+ z>8a9l5vztdi{hP7-XE(M0oXi3^_MQQ{yqW!>y7y)bPYL&!1_U!{Yp$`X5hG>q_fP? z3IB_kHR2k0x(7fe?knyi?hA?@0Gm$((1a9fVFq*lQH>M41!-PxmbbEVY-8zAxI&7H zxYCDCFu)+zDBL%k2f!(4|KETAqX0w9ab1rTL_33IkoM1ef`3n_@%2a&TLKs~vqG57 z{{H)OF`@-4!zBw~qk**mkd0>();VEYS#hj1>*PIhsMEHYEP)12wQ%$?6ewOszYJVI z6vps;6nRW29AUnko@k+a9eoMNBKBxvuv7nr!IKCurr*8OXxsPi-pe|NISmPs`-os& z0wX}uDk?0JUCoVMDGvCZV95Ery0Gs5iR>jFNKp+1ZOpN(SjKGudgnain*h*rPWs#1 zj@AvnX`&ndA` zT#c)tQBTHmz7pr`i1roKZopoEW%Rv9Sv)H+toJ#(K*pwcPzT4Te7%`qsX&^W#qP|?zfv01}r8XyXt#rL8=)lLq62IgiD zL>8brz%!2$Gc7TUfjpZy-L!j@VAO8o>khygo|Gw0O)~p=m}=fegtdBmiHftLz{XNhR%%j+&r9DdyKG>!j710iI5z)*rD#0U7`*z!fjdD> z^)bv9O=-4-M}5BN+)a<~hGqiZ1?b{ht)t1L`$*{Pu49p9E1G`2u4`v+gzLRFO}u|b zAx3olS6U1ZVBz<~s>rwwxTeQ<+QN%`$hALe!T(9i(C|n|`S)oX)8~bVPPoTSGRkNZ5+FsXDUs6+p==F&r;|H3g3kWGVj8(naF_-uX9FaGMaTR`uCWz_I&?bqI1BtV<|>;NN$x^Pg$^`R43|d(A2nzY6?urid2-+|&_k@(XkN z_jAOY|1K1Bl$!ptfK=SqbW9CxUtH^2;}lZTCErc^Z(^Z2vbd8U9;2|L8UZ!)}cu8GIZN9&u|Xt zu0u}o{STq@|M0P8+^;GHoMG_KJNkJ}e#4q{ZAD}}TQK+4r{{wdnK4%7S#1S8j})b* zkJ88!a4^M~#w|Om>RRajp#Nk2aqT}6K{PV+iOCP{nvAAzU&H-sus*3P_eJF;Po$kR z|9!4Uzw19WG>{-bb)ha#XB9Bd`*pQ`{mX8IMsfiA`# zRiE%*kD4X|#;`RB?-DkfO}X+l-N>dKw+}=*bcQiisJ%QYKfQYX+^fvkX`aC2sztOT z$&vR2|J)0LyW^3^m>(Rd2yig)KX`e04`#-ctBOg-Qbe<~XUC;qT~3-IXqApg)HV9L$^0 zWy7ZF$M8PV5oZOv?gs6h{_WP70M_;0Mk6=QDB?~7r6eXXk9?=>wNY!>8U_?O>3Hvr zQO7CkEa_%k2nvA`g&O*HoX ztcxyhtKIBz)u}d%NQ>+g?SOBLZ2@)(uFV(I<-4h!1N4pS_`}yPVu9sKe%`z3(j~vT zOo{{A$&W^qNt9dq$7LFb;JYh2*83XGXW$rf0@ur>8B!crDX0|FxoF^+m^a$}!@0U= zL@SA5GsCpP+=5!~j%aG>l<&1%4>wE$=tFl>HSO;O@I#+`Z6=0d?zYqv5P*JAmB0V~ zSpbON$-I_)?!aEam_`xE$CaC8b~@1*f8~88+TUDgY$21Iq5!}#vwgAPtcig6g^u{Y z|NXz>SAs$i&KETQGn_vPlrSi}ugM*EjJ}^sjlD02Y~h8X8H#z zyBe`Rkd8m13ZeZ6n8Z4!^V-&NW5dvZIoCL>=e`7(b1kV=3(+d~d1pm!PlW+{2;F|n z9~8xLtYBIQ$W0CZs5hs2H^_`;iF!=bwCmNC6@Ap8_PvaQ#*zpyzro_cXA~U8xH1$} zsG-3w9}gJ5rd~RYCFrcTYCwqMBi0-g9=}-kAB84057k5jtdG8Kr9~s^DH-n6iI3VJ zb6W)1m%cx7Ijj9{?ttX|j;Ycg-MOyPnCnME89 z|F_`$$XBx{5r^Ofj;E(pNd!BqAme#a-<%u&B-6h=DUCM`{+q8RVYu02x~<`*OHjAa zljT__Uk4jtb!TPLfYfRFi<$twekz+YY@X3=z$OWs0c_&j0sXxg1HBV4brt{$HcX)Nzrw+dM4sB|2pJWF5W-K1Fj)O~? zXZSkC1i�l~{K~BQ5uPAw`|XPaaFmAG*z_CUx-l!F6u3NW9y8_T+OrE%<>cb9s3! zYlQ&Q07~Wl!!$2^4UCnC(mnqv0Oq zv=;0fXp65xSfd5}i-*P=q1~%wo8 z;pc%UPdrOe5b*B5%6PsLHV;^8a!ld6dHiJyV#XEQ91!|A+!{Meqb4;`1YRSMNZ@N& z2gQTHNe!2bKdgUfJfq?YD3bgj3b-sKTKVaa|o z7oPLLs>DM@jXJX-UFvA$Eh_JEI_IaP<8{nB$#%>OQ37tlx`0jB^=k4x<~bI3k%C4& zO!1*UIUOJ(jxui6E}Os!yN9{7|5~bJx0=K|t?p}&Paf-8)9~So8VC1fsI6i-r)lb; z(xg88jB}?E?x=e#su@5@oKzk3r7>DgSq7Lqa?ScmJEhnJVS}VD;~%6mfQ`LF!&bDgg)iVH{8s;YH;wo_DME^ZTYeqfexb+_3 zf{Lj-CpX%2ZG9>mP>MZe+3QjWz^3U^859x;D5h(V#Z2n_%Xv&#`10jE^9@koyO!>W zYp|tYBTn|~))782N)%vxts7A>E?j*P8hZrunemr(sv!q3Nr6iUmUb>t9~|WtmE>ls z6akVAWb6i}*V+G`q!nm1UkmJ&+J7D8An68{6Bz(Dmm8<n*X6R6b~y6@bIH4d04>mk^sS)c< zdu_R(KvL#>bQ48a?)Z2(-7)f($RbTwD+47PGu`|w>?F)5O>WRaCBQ%KJAe_okzgU= z8bJDh;*L|Lv!slMXD;V_5ZXUmhyKXqI~tY8VOg`IOPn*8$@4Nk((JpLMdF&qsV6Nenm#X#Su7_Fs{U;hBIj3f(_6|Bx=A zkb!yT2A6(+7Fpye?_F`{B$^O&1%JmJ1XD}oY=K>Y?mtoraBogzE(OEe@16+Ig%MyH z5gA!KlO)vnwckns0ryD9xkvxuxV$KVF#x6=zK#SRk+FoL*5d+rr;ht&9A~Gg+Y=q< z_sQ~v8x5f75jc;z8P0p5$tayGJ9FBJNjvcygD9&{1foP*fE}IZuU7Bx@gJ$3q7g89KQ(fa=xX-=hb*R^#C`82f#n4N}3@+ z&z-F)kGLRhbC4=8;br=F2+ z@M*reV(t>!?6vsyzb<#29VCZLJm$_KxJz8Z`xnU$RZT5(*V7KEgIMoqD@}eIyJ$~I z4Szr@J~yK+YzzSKK}kkYKs((ZrK=4J?v=>}{tWT~7zBw!df2>p9{M9h3ZmmGCPHVj ziD>S!(9+dFN;^p#+Qe9?ChEwyF<~H2hN*X{+rSjtv?bC2BCb%Mnj~5f$$1*97%9jO zl*{Nr*4RD(OPm8XXOKm>8~t?z`RhoJ)xybTbe)&4n;u>}j8I3$1DAC@T^6hRt6!=gyk|8Vf;fJ zrkCnnuw3cu0Q@ml9AvAf!m4FEoZKR7fYv{|l zrvY!1^UCLzGY+&kvy9Ye+)rfGf0Cq$kCHecjG0d#lj52d*f8g`>qt_JI5|@dKZ+Jk zXBP@^0L@}TXbA-B%6&(EuNGW3daQ}laN0F2A7fMYt~^3FTPp-)MahUxbiOJ$*6Rjr z>kj|uhQq8BtRocEFxGL6y(nclhewXT&T<>Qn9uci4O(FGW=z5EMR_s z0f6U1W2Srh{M6V^jBWf3cIp*}t5tnTNl&Xb^eq58|ATruNN6(68Tn;^_9xX)O zC}t`{LaO7Ob~4*{H*JkE%N^Kb&j;O?v!8aW~kKgODGA%bB?5g`4Og2vk7ATHCQ zi@v9w9W$juhq9%ozKn%{4@P+h-5tOduWR0@xz&j{lHZpN3^spHEQ4Gb0uQA?aAx2} zjmZ|pF{S`^(J+t6Vd*f|dkcWsB#-id?O-pX=D@08^~h+XcN403eh*Sg^Mz^Pr3-wm zi~#XtxU{cZ!N5DlZbW#M6bMpEyfX-{tcc)|b|p>zTlnJ)1p(w1v*$$sm0`3}2iLHt zU|m6f3*2$--Jnk+7w^WHJ1;Qhz7ToGzO5*8?D!2&Ij*FQaisI(Ac5<9#D=u}{ym(pga0yu z@@r63(|AAq%rS)kV*!!|_^v?u0R{-x0GD_C`14QAKLYI^+G~tyr~Rkh{uKYfhOW6g_&twgOWPEF{c3d5&ArOgw+WT7<&B z%E?y3&yx~{f;od0ie~5Q`;Nu$tc|3+KLK8Cw zISsR?F}dPAH_v?B^QdEz&t3uuYOYYxy9o7x^YEJU)lJ4b(CF#L(}yqj19fwZ zmKMO{_4Uk$=D^PnwV&+BJT7|W=C%CaVo2h?t1sWG_rRFSc~v4{8%Y6TMl;6Wg&t@* zUVCH?)zhfhTO&+Mb*hs)k2(Nwh_e0U$L=l8$VX=(JPAs-31yW%s%;ysAA2(wH(bt*Lf)7WOq|HpF8$fGGgo z7ytnNYcqDcHrM+r&$Rb{i{YRZ4X(5};M#fbs%Hg&%c~f7=LUP_{pPGo??u1-yqjL? zZ&M_VP@>(Y1nlLY)1?P8wnS>;_t>Q%%Xqz3_B{Zh6#RQfzm=<4!Uq09mZo|p-u z(5!Loo1D4fojuRbb|c7CW>R+c+mE8txNQqC&hS{98K4(>FbCA&>z#D?B~}uqk)JeY zJJt8&4>tRlUyrR|=Gt4#6 zq5J0>ewrok@3epK(622fPdrxw5Pc~ze}Ai=L3l0JR}@Kq3gBNm|4(I&bPt4$6`2-E zJZRsRfDiTz)a~oi;{K$gE|l^2a_rFj{~kUd1HgO-%j^4aZ&0KGxW@U2AA^~x4y!18 zlA?LH>}`>xqWH_focd%M`c zW+2wrrO7GM*kNvV6xb{D`Z@jS{|j5p@iBe-_v;VQt>Qfom=6+yIsO?lsk=Jsr}Gp$ zL5);*NaBfVbDDI84p*2TF7n%+&Es8|wjmh^w5K2iSYNMZ19a|nPt2T$Qu+F>7!B@* z_|>HaTxrpedZqVC#W_R4)D4p@vIjFMz^Eue9zQ9VcMmY4k%OM67w0Pc!C9s!yj!nB zV`ENsI!Tc0bj)xneb`NN>K`JC5Ow7%-MB{Hl?60mA?D!?Ceef|mS?6AWp<+qQYnx0 zq=w=K7FvG%)?+bsdKfn5-`hrA^~<0!f9|G_LYv#ejM^-3PPYwKDoHL3E(m}MSPRuVH!&*$aGM=&RMvbRzTRK3Qyc^JeFw&j%c*uuMWR0-E*zgfxcdzx^g4 z7HbNMRRGmUEWml-exbPQMcXDR%#730@lK$Uj=QDJTZz~M_z&|B-E~~+R}uU->Kzdb z34aRS3Yc>kW5;(?q9F1aTRS=qdpq^CWmFki1ne?OQcwiOI1t@`I3^Uxy%31!AjZ0Q zCWNtoqNXx?Y~4zp`>CJCIG#Pltt-Zq7cSwg@!29qFU+G3Ci#}9O4KWs_>N7Qu zb3TMya%pJBwczqR4sORDwnyU`_c>f?3U-PX2ax|y+Dr4apPAxMz=JkwJc>SQm8(*d04tW<&V_)7Q8yN zIZhhZ)pax9q2Cx2rO_uADKce-lW;iIz*5E$Eepc^PtlocAm139XRL!L6;`~wC+~k^ zVBXBv)uJeAR&vGo8VUDjwepntG5vgbWYR+o`nLO-!i2<3iPYV99l>qfxP||r3$S{) z6436h;@-VOmlM(e?P{rR7oh*6n3-L~4}cedHKA>LF+e;6bhGiuD9^~eM*i{HeKix5 zm_BoNZw%yk*LKToydV#C_X;X!U-VttCEeAm(%+8`YB$h4$;pT^H+Y>2Kdf4j}Wa0-gkB7rY?x!ZIB{F77%1^7wFmCATZiXE}0 z<42G@B2#unvuJ%9n%wZP{Med9Ho#*UNtGHwX4fy-oKRB%CB`z@a2g%iZl1-&4$UV zkfX~lbMJ#XcQ4_G08rehzeY>*O*iR>?rd?lfLgfpq*4{tJ}+bpuGf0~l%D-sI?15I z;~W8+u@0d4@${*tlb{pXGCWqX6HQ6JeNHVOSNs^?(U~UPONdCZV1ptl%<_H%Xb$5A z#UPDkeJH>gsSY?_6i0Cl@CSy7G2YG#Qv*zn-aQ4^Vmtdc{%*|YzABpN(Fj#+9GRJrhQC95twPkBv?YE&Y0op&FNjPRa zvrtTaY=sIOCs-CR0`&wPP8R--XT%Wb;;!D_3pQ?iDC{_lcT4aeYBWUy&vs zCXw6RP#ZzG%Rgs_RIRff@fbEo-iv~tPa7Yp=OGZ{;N=YxPMVf zFZNvSJ&#P?;IE=mzX^>nkK8_S!(WBF#RpbT(M;XatV@4Gy47eX*V(M)@oE4co{OHk zoC5>_v^=pa@<)$f$|eUz0AyK1bE!1;EBP7eTp~dr&<;W2J~_E7n+`|>#5cgYd>+*X z4Wj6}k6O2I-wb=cy}S0KQ=E60xME1w6q)wCL$*O3HcV%s<@%h;x(da{GS%{WY+DLm zI&6b-<4jG*=&|}b2P9TF*;w~Gnq9f8eO7qLK~)0c^bde5ST10}EQq3P0LOy@7&iUrngRe~?0b5KdK##6xTYCs(adAu@H;(M z9Y2S81o;H+ksr5TL!wDRHe<`}Iu4TJO&hD!7E46GzO}eVG0W5f02mV}AOiS%u>`;e z=LrykZm2ro-*CRxh-O8y0#XTHSu7#0H?HrGKm8~` z8}mhz6;?5(6?1-J#pa#Ynuon*V)S45Sepa{RQp$<;013B93#dtUW0LfasHjp|GW2M zkq{s5_dqlpmPlO5YmYJnitCydBR(2)NelJ(9?VBI)#i~rk7d1DEMsI~T(9S@BL~pX zA4Cv;kVwFr1OK;tDuZTwvkq}|V~uYwOK}*J%u%Y$lOkl-W`OO&d{an$qN`c=q7b!>&XB- zdcg9HV#eC$r^6PQ9pT3#0zIZi0*;ei+#I0hL#6qR|C!jW;BHU^?OLF?YaZmlkCV}# zfTq@geHpcuF^k?Py8!FhWc~2N_mK|mRA@*79Q=bu3R=5HeqBl|6E?IbkpYIXtKHOH`^(qo_PXu&7m3S%zNjP;fF$X_Bl}t)o`Cd@8*cr07N+x00br9n zUouDivJ=mom?xI3hq4HeT=Fe<69=|jEaGiJ!r2LNyjs-X)<7xf{Oj#nY5Lb{(5Pec zi+l!Hl~_ml{6)sGQ}Dm|=kWdW@Y!9o0W7eA#&`y$7r+Pr9mnD90RSj5M6^T+KgU?y za>{r3I_4KP<`)+~%KQ^EL2>O?-9I+!qT3DnT@(N=1VCO#U2g=CM1$R$T2fD~J4=c2 z+9F{#c*eNPQE>fW7K${4^Uvq)amjHjr5y7Y#Si=}t`UsczO!z)raK(V_hK|#eB5$X z>d<*UC7Q#1A#}}0B~5}@SjY6@i)!Ux8#4<24FG+3tt6v_4l6p*(91)I?nZP`!ZP7O zG5^`d+4CT$)9d}jeLNS#zrUr30Tu)3>zFebZzx{A}{BaeqE12d{gvv*B_3 zhiDROZE$C$iNRtxRun0~0Qea00mkTqF#m9^!BoK5!aUK$j&5O*8XA{N)gw!2?0Mr4 zV|=YO{(CJ2AvfH^aXfnbNDJtX)%*|7JNJzcn{Iz*^cXu+kNVKfK;4^0z`x1!AU_c2 z{}1X%g7?Ss8$LY`r2zj><{o@DVggR~Jfq?{IUGmXJ~z89Rc^*7o>ii=cC%oII{Y&+ z))*P-Igz zL%fuyX*hcHh%{Q;tFr%h&d8;=9^6tzo;MZ2Y>+-I{Mh$dhDHpC5aRaG|cFR z{^7kvpM4h4B#!rC6N0a!W8?8|=!C+90H4vVk@&NF_oPDzpMZN7hbEulSR73SRf9`Z z@n_&Zs(ntQuV|I3)yOm}x_jNrl!BzgUnFqk8_x=bo|0z)b&V>ip&orL=j?~17?4H! zF%Pa&)YMG?JMQ|OoEffA@^fl_`aJ9u1QJel${5%dY=x$f8pMwUW%TJN&A@wXwy~>! zDP#5`f_X&txq06wi|zw}Rp5Uv<5eRXHFnRl$FYIG8^$X>3#%n;rkx>!u?tXyIRGHw zYyq4H-WLD?uHWi!pazrcwNn1E5l5^hEz}nS#|}^la1F2lu#9f912>{Y7DHD?#kHFW zu1bt*2{+uN>_rL!wV-7_ejAhW)VOR(T!{Y$3aW9=R z?giGSvu@P(H$2)>-q*<(Z`ruM{!;vBP^iH52KWaH4c~k4P-PbX2oePx@52WVqo;w7 zopxRBFX)dE7N&er@#JW7xtz%3!u-c|g=_~@dj@e zdobvQ3`G(M!ZkxN2Q0gLp`i01tXU7?&vXBt6y`jt*VmD9+dO70@|1-0vDRW zz49Sa&}Qsmdw~I$__D5nr$bU2>TSuPW@Vr663^{@7+*!>GYoj zM08!n67;L?0GYLZQk*OuIx_IjDX`<=QkBJ4%2D-6WD0*9%1!nXsUvaR-lp%g)&GNO z6}+_uJpgWxH+~aQdb8pv<`^@4Jb& zzh@NaduA92(1R|NFa|7I(6z#Dz_mdql2n&E&CV+bTsIdg`D?u z2WH;K5R@nSQVtYE2dRKOL7>-EVg(mF^2^O2a@~~|Y&S|zzcM<%JNE;eiYb=4wSO=M zix^s(TfKn(%{rQM#i}_v{wJrRF$S=U@t5N0ude<8wE(|hyFk){Kak1beZEKm;G?NQpm@ebe*n0* z*&Ia#D2f4O#(z(Y`j2B2!0oY?t@Fv=40uWm60eMj@b-XxC}fU`Aoz7B1AO_S{@uIq6!1}#I&aS9PBnnEq8zDN7 z(#S+jR{%h8?YWpzeo-09g?0QdjqWdul`q=m$9VIII`OX%EiQ>h{XVF%Mvun#@fn+C zbj<+%Vde)ncNn~Z34mmav(Qz;n7lIm{(X$~yC0=XF7b^Gj zNJIcf%%Qh|G7R7iun#)#^XJdyXQUf@svCIB1vTK)?m4F&?6lLLQR^uKMv+yqXeA~- zw{g6w6av>zudkh#&0!pu%*g=0knn)x!nJv9=AV#A;QB#_ED>eb>r9(nn*2@jGuY8Z zA0vIsbls*CUx?JGF{#6annDJk2X+UN4M6|LdbTuX2z(yUiGKF|GdW)Y|0bE(S`ooJ zt(mXD4oTkZlpY@kl3Jh%8~8h}J;pwYHZT8rA;yIO|Ckdv7YEco{rs~OGyr6m(HkaB zAQ{MF=fOy%&MHT(bDN4-6Hto|Hi~r8{SO6~zk|m9JW}lM$+bp74h0x6$MCZ{CfAl! zv6v11(=MB*nYb}2iYQ>X0sOy#L1^f>$JOub4_cy}LyIET1hk9opA;;TpX-Rf3)9Qf!q z+@}}BrkGCOmYv#-y^eW=6MsLj9?AyG_DL0DepzPTRTC-qTbxrVN4gCp>0LA=m7Wg~ zya+(a8oUSwEGvKJMOG#CM~#d(L96{xesVMfzKXg-Meo!$FZx*d+zz1%!Ji9HfnKeC zr;1d>XYGyQnWyDf?!gx5d+7CP{&#IWdTC&@X_xaB5az?Zj`z5s~O}033UiSZ-mQC0&*tqifpnB*N0Z{1hNv3z`e0aBCV$Hs&RN&pa_sR|s z4*@LV?4?D~-BU_`=;i~w!Q>6}c5LdgX-AO@VD{NF#Dj(u~5ca7+(PW&!7J$=5t7A*qGD*<0p?b z;tdghLGFO{@7UtiHeTQ{%KsRb{QhTb8E4w_bCKfTWF8wRhT;b=Hy0w=`aKkseiI49 zU&7wNSOthiC;f*X)J0!_Ygj|s*cx6~s>*@-m`>c?(RJmIIb&*ThNfP8!5q|hy;pLq z0{$UW5c$m$x#oCAcsfD!N;$y3EWICYB8W7COYtOlFNKMhFaH)+g1h>kiuV7pfPZ|x zD~|gsi~QqEQbJ=fQXvZyojx~FrgUM+8}e)oa~4HU{1nzQbpBCT^fe2|Ld9k}%G3@P zO*cUM;;t5+DzCYXj{Yd(+LQ#SS+Aqf&i^w4rUn^$%rm^y5Ac5DYaQH6B%EMG&s>D*@ZQZ<>gy03s?>O3D$Txze_vC6Fu#NUZICR+7cce&|NToH^B#1)GCt41 zKUIvkqGNjR#*+k`W3z~^OsYu+#n5$<9;*gmg|6nix9?=L0-1mp1Fo&RdtpE?r8$ah zAb<(=5IEHiY}nZ72Iov@j!`NAb^r_pPn3#`E(jtIoxxBu{Hn4d8tz?`#i(k*yXOa2 zG@i(72R;xRC4qHPPEySyk^VXjZ#cDfs~pZMjHA$f_Uh!Q-JRs zeQbPDToUK`PoJe5b}gO2jmQGLL!NHt#c&ec>M(8qBw%=k4~!vfreTWqN1#XJy~La1 z?7o`3Nyq;rc5tu6FJK)V{BfQxQE}?Luol)E11O6O5c(WLmk~b$M!}C^Q;)HJbo4^! zOaK@trU6h&G3`-L?KTaNqTu=%-yykpu7ux8+nqkvh(A?3&_y0q&G4 z|GQAoQB;z2lfVK|m;WE76OD0$F@<-=I4Fw?J|22y!6TakG#tyE-DfWZp!1J0h7Pg# z7ldNQAAkHV^9J`zicX>H|8p?XLoq{*3y+lrVPIS~l?&S4Sm2S{8&42O3~9YUglp^( zvtEG0vjWA4J11%^38n%x_VDFE0lH+99I|&BjaZG zj1Pcr853S0OdB&*UzOn-@4(%kJGwSrN?`<@e-tIakiggROvajp!p;M8{s$9;Q_q#j zU~+%Dd5`_*jWnG12LE%trRea|2|mcrd|TjumPT;LyY)xKW?ouM`35*MH03>I=NXzV zN)3m?Ej)1E(F+HLP|63^OOPR%DgYq1h^7mIK(EX79^N<#Lx8SsPuMy z{OMTPNM~_?l(#2R8u~V-Gi(inYe%$<=hLV^b?kT0BwGHhcFwO2{43zs0suN*I#w35?2PGEQXkow$_x;G?CRIM7;{#n>bPxd~e)?Gp0Qfq*EU?K30Nx0#_eenR zsr{U1d9$$-WHWw!txoG!TsT_k<5uGGjEw+r9kbtUJgFpLEes6&8{>Fs=^P54F%MqL z@c;}RNj%_2(llToC`|<^9;{^Ebv5Q3>23;@YhxBH#?8z^{O#JILOi~X=yY(464B_01&JQmCs0B9KfJ^$^$ zQpkWu1^%Gmgdzr9^zm6W>vznkZfk5QOPt`vUP(bEu!wWP#;h0o9ALkYl!cdOs(CH* z7|#zd72y1j!V}hb2g5hxl|)%_4ZjCt14TJ769By3-vRRh$0V{BfdB9;lDI%;gP5Ek zH+CKFg1UjDHW(LX-p6r{;n^ugb@3rk?LYpwM*=)bU`Ti>g_4D>1C%|>Z+6c$-U#wr z@E73b0MzyagO%CXdRAlcrUBJ1*LeCrs+y)-!EZd=Ei8$kRC)k3S*indj7~G+>j3Gj zNB8A)ed7rij>KH2+*8NKfh*RP+%YSPs~DyGVD1T@-LdKDsH;oY$+;W}=3cW2PWUKH zJb50DrRDd@Q#ew(YtEC0AJB(C1I{s^KiN8U5$VZjGLbkAa>Bs<+G)ehpIE!J%93o+UP(9@&= z)o%;!+d&jMsg1f=fBdA01D;{O5Z(VuHeQ}MfTFe2%s{7I|Az5I{=$^?YNqJ z03Z>p26JwA&@c3EY_P-cgSP;R99}f&#EETN$L$u>bG5j_+qBS9ZtUA_pwslT5CqCi zzp|Opm&i#?CiO^*0j;Ax87R!x5wgxzuGwxK`~eo$7QuIIoo^I!kO<-3a=gX?D8RZn z^@sb9*Faw`ueH}-h(yGsA{RPNJGFk3=#!l#BA>0xj{*@kzzX_LqZ7Rs15~>5 zROBnT-|!9)$q0Z+Iwzc7*lM36xR!5`=i7QUPbdcIsT)g=!fAR89JWzJt&%eZY`egI!{M1`3;Usw8=?Z z#EUXGFNqKfphk)*;l1(u0EExXT^w^tO$iqrL%l;iH>g~vxt?3jJRFM}Bk?SeL8U

    ~bU{3)^!`nib438g2Z;7oGZ#c>`dfoGx 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

    Zq%a*Pi=04AdUL*rJ{UjEr@4pSUfX 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 Hu0mjfb6k4_ literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/nsis-hooks.nsh b/desktop/src-tauri/nsis-hooks.nsh new file mode 100644 index 0000000..e0bf9c3 --- /dev/null +++ b/desktop/src-tauri/nsis-hooks.nsh @@ -0,0 +1,5 @@ +!macro NSIS_HOOK_PREINSTALL + ; Keep per-user program files separate from VidXP's shared models and indexes. + StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${PRODUCTNAME}" + SetOutPath "$INSTDIR" +!macroend diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..c831e8d --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,1700 @@ +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + env, fs, + io::{self, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + path::{Path, PathBuf}, + process::{Child, Command, Output, Stdio}, + sync::{ + Mutex, + atomic::{AtomicBool, Ordering}, + }, + thread, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +use atomic_write_file::AtomicWriteFile; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use tauri::{ + AppHandle, Manager, RunEvent, WindowEvent, + menu::{Menu, MenuItem}, + tray::TrayIconBuilder, +}; +use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; +use tauri_plugin_opener::OpenerExt; +use tauri_plugin_shell::{ + ShellExt, + process::{Command as ShellCommand, CommandChild, CommandEvent}, +}; +use wait_timeout::ChildExt; + +const RUNTIME_MANIFEST_BYTES: &[u8] = include_bytes!("../../runtime-manifest.json"); +const RUNTIME_CONSTRAINTS_BYTES: &[u8] = include_bytes!("../../runtime-constraints.txt"); +const PRODUCT_DATA_DIRECTORY_NAME: &str = "VidXP"; + +#[derive(Clone, Deserialize, Serialize)] +struct CapabilitySpec { + extra: String, + modality: String, + label: String, +} + +#[derive(Clone, Deserialize, Serialize)] +struct SurfaceSpec { + extra: String, + label: String, + description: String, + default: bool, +} + +#[derive(Clone, Deserialize, Serialize)] +struct MediaRuntimeSpec { + strategy: String, + executables: Vec, + reason: String, +} + +#[derive(Clone, Deserialize, Serialize)] +struct RuntimeManifest { + schema_version: u32, + desktop_version: String, + package_name: String, + package_version: String, + dependency_index: String, + dependency_constraints_sha256: String, + python_version: String, + uv_version: String, + surfaces: BTreeMap, + capabilities: BTreeMap, + media_runtime: MediaRuntimeSpec, +} + +#[derive(Deserialize)] +struct InstallRequest { + capabilities: Vec, + surfaces: Vec, + prepare_models: bool, + model_directory: Option, +} + +#[derive(Serialize)] +struct InstallResult { + package_version: String, + capabilities: Vec, + surfaces: Vec, + model_directory: String, + prepared: bool, +} + +#[derive(Serialize)] +struct RuntimeStatus { + ready: bool, + package_version: String, + capabilities: Vec, + surfaces: Vec, + model_directory: String, + detail: String, +} + +#[derive(Clone, Serialize)] +struct MediaRuntimeStatus { + ready: bool, + ffmpeg_executable: Option, + ffprobe_executable: Option, + required_encoders: Vec, + errors: Vec, + package_manager: Option, + install_command: Option, + automatic_install: bool, +} + +struct VerifiedMediaRuntime { + ffmpeg: PathBuf, + ffprobe: PathBuf, +} + +struct SystemInstallPlan { + manager: String, + command: Vec, + automatic: bool, +} + +#[derive(Clone, Deserialize, Serialize)] +struct ActiveRuntime { + schema_version: u32, + manifest_sha256: String, + profile: String, + package_version: String, + capabilities: Vec, + #[serde(default)] + surfaces: Vec, + #[serde(default)] + model_directory: PathBuf, +} + +struct DesktopPaths { + private_data: PathBuf, + data: PathBuf, + cache: PathBuf, + repository: PathBuf, + runtimes: PathBuf, + python: PathBuf, + models: PathBuf, + active_runtime: PathBuf, +} + +struct ManagedUi { + process: Child, + url: String, +} + +struct DesktopState { + ui_process: Mutex>, + operation_process: Mutex>, + operation_worker_runtime: Mutex>, + operation_active: AtomicBool, + shutdown_started: AtomicBool, +} + +impl Default for DesktopState { + fn default() -> Self { + Self { + ui_process: Mutex::new(None), + operation_process: Mutex::new(None), + operation_worker_runtime: Mutex::new(None), + operation_active: AtomicBool::new(false), + shutdown_started: AtomicBool::new(false), + } + } +} + +struct OperationGuard<'a> { + active: &'a AtomicBool, +} + +impl Drop for OperationGuard<'_> { + fn drop(&mut self) { + self.active.store(false, Ordering::Release); + } +} + +fn manifest() -> Result { + let manifest: RuntimeManifest = serde_json::from_slice(RUNTIME_MANIFEST_BYTES) + .map_err(|error| format!("The embedded runtime manifest is invalid: {error}"))?; + let actual = hex::encode(Sha256::digest(normalized_runtime_constraints().as_ref())); + if actual != manifest.dependency_constraints_sha256 { + return Err(format!( + "The embedded runtime constraints have digest {actual}; expected {}.", + manifest.dependency_constraints_sha256 + )); + } + Ok(manifest) +} + +fn normalize_line_endings(bytes: &[u8]) -> Cow<'_, [u8]> { + if !bytes.windows(2).any(|pair| pair == b"\r\n") { + return Cow::Borrowed(bytes); + } + + let mut normalized = Vec::with_capacity(bytes.len()); + let mut offset = 0; + while offset < bytes.len() { + if bytes[offset..].starts_with(b"\r\n") { + normalized.push(b'\n'); + offset += 2; + } else { + normalized.push(bytes[offset]); + offset += 1; + } + } + Cow::Owned(normalized) +} + +fn normalized_runtime_constraints() -> Cow<'static, [u8]> { + normalize_line_endings(RUNTIME_CONSTRAINTS_BYTES) +} + +fn manifest_digest() -> String { + hex::encode(Sha256::digest(RUNTIME_MANIFEST_BYTES)) +} + +fn desktop_paths(app: &AppHandle) -> Result { + let private_data = app.path().app_local_data_dir().map_err(|error| { + format!("Could not resolve the private application data directory: {error}") + })?; + let local_data = app.path().local_data_dir().map_err(|error| { + format!("Could not resolve the operating-system data directory: {error}") + })?; + let cache = app + .path() + .app_cache_dir() + .map_err(|error| format!("Could not resolve the application cache directory: {error}"))?; + Ok(desktop_paths_from_roots(&private_data, &cache, &local_data)) +} + +fn desktop_paths_from_roots(private_data: &Path, cache: &Path, local_data: &Path) -> DesktopPaths { + let data = local_data.join(PRODUCT_DATA_DIRECTORY_NAME); + DesktopPaths { + repository: data.join("repositories").join("default"), + runtimes: private_data.join("runtimes"), + python: private_data.join("python"), + active_runtime: private_data.join("active-runtime.json"), + models: data.join("models"), + private_data: private_data.to_path_buf(), + data, + cache: cache.to_path_buf(), + } +} + +fn move_legacy_shared_directory(source: &Path, destination: &Path) -> Result { + if !source.exists() { + return Ok(false); + } + if destination.exists() { + log::warn!( + "Leaving legacy VidXP data at {} because {} already exists", + source.display(), + destination.display() + ); + return Ok(false); + } + let parent = destination + .parent() + .ok_or_else(|| format!("{} has no parent directory", destination.display()))?; + fs::create_dir_all(parent) + .map_err(|error| format!("Could not create {}: {error}", parent.display()))?; + fs::rename(source, destination).map_err(|error| { + format!( + "Could not move legacy VidXP data from {} to {}: {error}", + source.display(), + destination.display() + ) + })?; + Ok(true) +} + +fn migrate_legacy_shared_data(app: &AppHandle) -> Result<(), String> { + let paths = desktop_paths(app)?; + let legacy_models = paths.private_data.join("models"); + let legacy_repositories = paths.private_data.join("repositories"); + let shared_repositories = paths + .repository + .parent() + .expect("the default repository always has a parent"); + let moved_models = move_legacy_shared_directory(&legacy_models, &paths.models)?; + move_legacy_shared_directory(&legacy_repositories, shared_repositories)?; + + if moved_models && paths.active_runtime.exists() { + let contents = fs::read(&paths.active_runtime).map_err(|error| { + format!("Could not read the active runtime during migration: {error}") + })?; + let mut active: ActiveRuntime = serde_json::from_slice(&contents) + .map_err(|error| format!("The active runtime pointer is invalid: {error}"))?; + if active.model_directory == legacy_models { + active.model_directory = paths.models.clone(); + write_active_runtime(&paths, &active)?; + } + } + Ok(()) +} + +fn model_directory(paths: &DesktopPaths, requested: Option<&str>) -> Result { + let Some(requested) = requested.map(str::trim).filter(|value| !value.is_empty()) else { + return Ok(paths.models.clone()); + }; + let directory = PathBuf::from(requested); + if !directory.is_absolute() { + return Err("The model directory must be an absolute path.".into()); + } + if directory.is_file() { + return Err("The selected model location is a file, not a directory.".into()); + } + Ok(directory) +} + +fn selected_capabilities( + manifest: &RuntimeManifest, + requested: &[String], +) -> Result, String> { + let selected: BTreeSet<_> = requested.iter().cloned().collect(); + if selected.is_empty() { + return Err("Select at least one capability.".into()); + } + if let Some(unknown) = selected + .iter() + .find(|name| !manifest.capabilities.contains_key(*name)) + { + return Err(format!("Unknown desktop capability: {unknown}")); + } + Ok(selected.into_iter().collect()) +} + +fn selected_surfaces( + manifest: &RuntimeManifest, + requested: &[String], +) -> Result, String> { + let selected: BTreeSet<_> = requested.iter().cloned().collect(); + if let Some(unknown) = selected + .iter() + .find(|name| !manifest.surfaces.contains_key(*name)) + { + return Err(format!("Unknown desktop surface: {unknown}")); + } + Ok(selected.into_iter().collect()) +} + +fn package_specification( + manifest: &RuntimeManifest, + capabilities: &[String], + surfaces: &[String], +) -> String { + let extras: BTreeSet<_> = manifest + .surfaces + .iter() + .filter(|(name, _)| surfaces.contains(name)) + .map(|(_, surface)| surface.extra.clone()) + .chain( + capabilities + .iter() + .map(|name| manifest.capabilities[name].extra.clone()), + ) + .collect(); + format!( + "{}[{}]=={}", + manifest.package_name, + extras.into_iter().collect::>().join(","), + manifest.package_version + ) +} + +fn base_package_specification(manifest: &RuntimeManifest) -> String { + format!("{}=={}", manifest.package_name, manifest.package_version) +} + +fn package_index(package_version: &str) -> &'static str { + if package_version.split_once('-').is_some() { + "https://test.pypi.org/simple" + } else { + "https://pypi.org/simple" + } +} + +fn package_acquisition_arguments(manifest: &RuntimeManifest, python: &Path) -> Vec { + vec![ + "pip".into(), + "install".into(), + "--python".into(), + python.to_string_lossy().into_owned(), + "--no-config".into(), + "--no-deps".into(), + "--default-index".into(), + package_index(&manifest.package_version).into(), + "--index-strategy".into(), + "first-index".into(), + base_package_specification(manifest), + ] +} + +fn dependency_installation_arguments( + manifest: &RuntimeManifest, + capabilities: &[String], + surfaces: &[String], + python: &Path, + constraints: &Path, + cpu_torch: bool, +) -> Vec { + let mut arguments = vec![ + "pip".into(), + "install".into(), + "--python".into(), + python.to_string_lossy().into_owned(), + "--no-config".into(), + "--default-index".into(), + manifest.dependency_index.clone(), + "--index-strategy".into(), + "first-index".into(), + "--constraints".into(), + constraints.to_string_lossy().into_owned(), + ]; + if cpu_torch { + arguments.extend(["--torch-backend".into(), "cpu".into()]); + } + arguments.push(package_specification(manifest, capabilities, surfaces)); + arguments +} + +fn capability_command_arguments( + manifest: &RuntimeManifest, + operation: &str, + capabilities: &[String], +) -> Vec { + let modalities = capabilities + .iter() + .map(|name| manifest.capabilities[name].modality.as_str()) + .collect::>() + .join(","); + vec![ + operation.into(), + "--json".into(), + "--modalities".into(), + modalities, + ] +} + +fn executable(runtime: &Path, name: &str) -> PathBuf { + if cfg!(windows) { + runtime.join("Scripts").join(format!("{name}.exe")) + } else { + runtime.join("bin").join(name) + } +} + +fn executable_candidates(name: &str) -> Vec { + let requested = PathBuf::from(name); + if requested.is_absolute() || requested.components().count() > 1 { + return vec![requested]; + } + let mut directories = env::var_os("PATH") + .map(|value| env::split_paths(&value).collect::>()) + .unwrap_or_default(); + if cfg!(windows) { + if let Some(local) = env::var_os("LOCALAPPDATA") { + directories.push( + PathBuf::from(local) + .join("Microsoft") + .join("WinGet") + .join("Links"), + ); + } + } + if cfg!(target_os = "macos") { + directories.extend([ + PathBuf::from("/opt/homebrew/bin"), + PathBuf::from("/usr/local/bin"), + ]); + } else if !cfg!(windows) { + directories.extend([ + PathBuf::from("/usr/local/bin"), + PathBuf::from("/usr/bin"), + PathBuf::from("/snap/bin"), + ]); + } + directories + .into_iter() + .flat_map(|directory| { + let plain = directory.join(name); + if cfg!(windows) && plain.extension().is_none() { + vec![plain.with_extension("exe"), plain] + } else { + vec![plain] + } + }) + .collect() +} + +fn resolve_system_executable(name: &str) -> Option { + executable_candidates(name) + .into_iter() + .find(|candidate| candidate.is_file()) + .and_then(|candidate| fs::canonicalize(&candidate).ok().or(Some(candidate))) +} + +fn combined_output(output: &Output) -> String { + format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +fn required_encoder_missing(output: &str, encoder: &str) -> bool { + !output + .lines() + .flat_map(|line| line.split_whitespace()) + .any(|token| token == encoder) +} + +fn system_install_plan() -> Option { + if cfg!(windows) { + resolve_system_executable("winget")?; + return Some(SystemInstallPlan { + manager: "Windows Package Manager".into(), + command: vec![ + "winget".into(), + "install".into(), + "--id".into(), + "Gyan.FFmpeg".into(), + "--exact".into(), + "--source".into(), + "winget".into(), + "--accept-package-agreements".into(), + "--accept-source-agreements".into(), + ], + automatic: true, + }); + } + if cfg!(target_os = "macos") { + let brew = resolve_system_executable("brew")?; + return Some(SystemInstallPlan { + manager: "Homebrew".into(), + command: vec![ + brew.to_string_lossy().into_owned(), + "install".into(), + "ffmpeg".into(), + ], + automatic: true, + }); + } + if resolve_system_executable("apt-get").is_some() { + return Some(SystemInstallPlan { + manager: "APT".into(), + command: vec![ + "sudo".into(), + "apt-get".into(), + "install".into(), + "ffmpeg".into(), + ], + automatic: false, + }); + } + if resolve_system_executable("dnf").is_some() { + return Some(SystemInstallPlan { + manager: "DNF".into(), + command: vec![ + "sudo".into(), + "dnf".into(), + "install".into(), + "ffmpeg".into(), + ], + automatic: false, + }); + } + None +} + +fn display_command(arguments: &[String]) -> String { + arguments + .iter() + .map(|argument| { + if argument.contains(char::is_whitespace) { + format!("\"{}\"", argument.replace('"', "\\\"")) + } else { + argument.clone() + } + }) + .collect::>() + .join(" ") +} + +fn inspect_media_runtime() -> MediaRuntimeStatus { + let ffmpeg = resolve_system_executable("ffmpeg"); + let ffprobe = resolve_system_executable("ffprobe"); + let mut errors = Vec::new(); + if let Some(path) = &ffmpeg { + let mut version_command = Command::new(path); + version_command.arg("-version"); + let version = checked_output(version_command, "FFmpeg version check"); + if let Err(error) = version { + errors.push(error); + } else { + let mut encoder_command = Command::new(path); + encoder_command.args(["-hide_banner", "-encoders"]); + match checked_output(encoder_command, "FFmpeg encoder check") { + Ok(output) => { + let encoders = combined_output(&output); + for required in ["libx264", "aac"] { + if required_encoder_missing(&encoders, required) { + errors.push(format!( + "FFmpeg is missing the required {required} encoder." + )); + } + } + } + Err(error) => errors.push(error), + } + } + } else { + errors.push("FFmpeg was not found.".into()); + } + if let Some(path) = &ffprobe { + let mut probe_command = Command::new(path); + probe_command.arg("-version"); + if let Err(error) = checked_output(probe_command, "ffprobe version check") { + errors.push(error); + } + } else { + errors.push("ffprobe was not found.".into()); + } + let plan = if errors.is_empty() { + None + } else { + system_install_plan() + }; + MediaRuntimeStatus { + ready: errors.is_empty(), + ffmpeg_executable: ffmpeg.map(|path| path.to_string_lossy().into_owned()), + ffprobe_executable: ffprobe.map(|path| path.to_string_lossy().into_owned()), + required_encoders: vec!["libx264".into(), "aac".into()], + errors, + package_manager: plan.as_ref().map(|plan| plan.manager.clone()), + install_command: plan.as_ref().map(|plan| display_command(&plan.command)), + automatic_install: plan.is_some_and(|plan| plan.automatic), + } +} + +fn verified_media_runtime() -> Result { + let status = inspect_media_runtime(); + if !status.ready { + return Err(format!( + "{} Run the guided FFmpeg setup, then retry.", + status.errors.join(" ") + )); + } + Ok(VerifiedMediaRuntime { + ffmpeg: PathBuf::from( + status + .ffmpeg_executable + .ok_or("FFmpeg did not resolve to an absolute path.")?, + ), + ffprobe: PathBuf::from( + status + .ffprobe_executable + .ok_or("ffprobe did not resolve to an absolute path.")?, + ), + }) +} + +fn clean_environment(paths: &DesktopPaths) -> Vec<(String, String)> { + let mut environment: Vec<_> = std::env::vars() + .filter(|(key, _)| { + let upper = key.to_ascii_uppercase(); + !upper.starts_with("VIDXP_") + && !upper.starts_with("DBOS_") + && !upper.starts_with("UV_") + && !upper.starts_with("STREAMLIT_") + && !upper.starts_with("PYTHON") + && !upper.starts_with("PYENV_") + && !upper.starts_with("PIP_") + && !upper.starts_with("CONDA") + && upper != "VIRTUAL_ENV" + && upper != "VIRTUAL_ENV_PROMPT" + && upper != "_OLD_VIRTUAL_PATH" + }) + .collect(); + environment.extend([ + ( + "VIDXP_DATA_DIR".into(), + paths.data.to_string_lossy().into_owned(), + ), + ( + "VIDXP_MODEL_CACHE".into(), + paths.models.to_string_lossy().into_owned(), + ), + ("VIDXP_ALLOW_MODEL_DOWNLOADS".into(), "true".into()), + ("STREAMLIT_SERVER_HEADLESS".into(), "true".into()), + ( + "STREAMLIT_BROWSER_GATHER_USAGE_STATS".into(), + "false".into(), + ), + ]); + environment +} + +fn configured_command(executable_path: &Path, paths: &DesktopPaths) -> Command { + let mut command = Command::new(executable_path); + hide_child_console(&mut command); + command.env_clear(); + command.envs(clean_environment(paths)); + command +} + +#[cfg(windows)] +fn hide_child_console(command: &mut Command) { + use std::os::windows::process::CommandExt; + + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); +} + +#[cfg(not(windows))] +fn hide_child_console(_command: &mut Command) {} + +fn checked_output(mut command: Command, operation: &str) -> Result { + hide_child_console(&mut command); + let output = command + .output() + .map_err(|error| format!("{operation} could not start: {error}"))?; + if output.status.success() { + return Ok(output); + } + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + let detail = if stderr.is_empty() { stdout } else { stderr }; + Err(format!("{operation} failed: {detail}")) +} + +fn active_runtime(paths: &DesktopPaths) -> Result { + let contents = fs::read(&paths.active_runtime) + .map_err(|_| "Local video processing has not been configured yet.".to_string())?; + let active: ActiveRuntime = serde_json::from_slice(&contents) + .map_err(|error| format!("The active runtime pointer is invalid: {error}"))?; + if active.schema_version != 2 || active.manifest_sha256 != manifest_digest() { + return Err("The desktop runtime needs to be installed for this app version.".into()); + } + if !active + .profile + .chars() + .all(|character| character.is_ascii_hexdigit() || character == '-') + { + return Err("The active runtime profile identity is invalid.".into()); + } + Ok(active) +} + +fn runtime_directory(paths: &DesktopPaths, active: &ActiveRuntime) -> PathBuf { + paths.runtimes.join(&active.profile) +} + +fn write_active_runtime(paths: &DesktopPaths, active: &ActiveRuntime) -> Result<(), String> { + let mut destination = AtomicWriteFile::options() + .open(&paths.active_runtime) + .map_err(|error| format!("Could not stage the active runtime pointer: {error}"))?; + serde_json::to_writer(&mut destination, active) + .map_err(|error| format!("Could not serialize the active runtime pointer: {error}"))?; + destination + .flush() + .and_then(|_| destination.commit()) + .map_err(|error| format!("Could not activate the validated runtime: {error}")) +} + +async fn supervised_output( + state: &DesktopState, + command: ShellCommand, + operation: &str, +) -> Result<(Vec, Vec), String> { + if state.shutdown_started.load(Ordering::Acquire) { + return Err(format!( + "{operation} was cancelled because VidXP is closing." + )); + } + let (mut events, child) = command + .spawn() + .map_err(|error| format!("{operation} could not start: {error}"))?; + { + let mut active_child = state + .operation_process + .lock() + .map_err(|_| "The setup process supervisor is unavailable.".to_string())?; + if active_child.is_some() { + drop(active_child); + let _ = child.kill(); + return Err("Another desktop setup process is already running.".into()); + } + *active_child = Some(child); + } + + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut exit_code = None; + while let Some(event) = events.recv().await { + match event { + CommandEvent::Stdout(bytes) => stdout.extend(bytes), + CommandEvent::Stderr(bytes) => stderr.extend(bytes), + CommandEvent::Error(error) => stderr.extend(error.as_bytes()), + CommandEvent::Terminated(status) => { + exit_code = status.code; + break; + } + _ => {} + } + } + if let Ok(mut active_child) = state.operation_process.lock() { + active_child.take(); + } + if exit_code == Some(0) { + return Ok((stdout, stderr)); + } + let detail = String::from_utf8_lossy(&stderr).trim().to_owned(); + Err(format!( + "{operation} failed{}: {detail}", + exit_code.map_or_else(String::new, |code| format!(" with exit code {code}")) + )) +} + +async fn uv_output( + app: &AppHandle, + state: &DesktopState, + paths: &DesktopPaths, + arguments: Vec, + operation: &str, +) -> Result<(), String> { + let mut command = app + .shell() + .sidecar("uv") + .map_err(|error| format!("The bundled uv sidecar is unavailable: {error}"))? + .args(arguments) + .env_clear(); + for (key, value) in clean_environment(paths) { + command = command.env(key, value); + } + command = command + .env("UV_CACHE_DIR", paths.cache.join("uv")) + .env("UV_PYTHON_INSTALL_DIR", &paths.python) + .env("UV_NO_CONFIG", "1") + .env("UV_MANAGED_PYTHON", "1"); + supervised_output(state, command, operation).await?; + Ok(()) +} + +fn run_vidxp( + runtime: &Path, + paths: &DesktopPaths, + arguments: &[String], + operation: &str, +) -> Result { + let mut command = configured_command(&executable(runtime, "vidxp"), paths); + command + .arg("--index-dir") + .arg(&paths.repository) + .args(arguments); + checked_output(command, operation) +} + +async fn run_vidxp_supervised( + app: &AppHandle, + state: &DesktopState, + runtime: &Path, + paths: &DesktopPaths, + arguments: &[String], + operation: &str, +) -> Result<(), String> { + let mut command = app + .shell() + .command(executable(runtime, "vidxp")) + .env_clear(); + for (key, value) in clean_environment(paths) { + command = command.env(key, value); + } + command = command + .arg("--index-dir") + .arg(&paths.repository) + .args(arguments); + supervised_output(state, command, operation).await?; + Ok(()) +} + +#[tauri::command] +fn runtime_manifest() -> Result { + manifest() +} + +#[tauri::command] +fn media_runtime_status() -> MediaRuntimeStatus { + inspect_media_runtime() +} + +#[tauri::command] +fn choose_model_directory(app: AppHandle) -> Result, String> { + let selection = app + .dialog() + .file() + .set_title("Choose where VidXP stores model files") + .blocking_pick_folder(); + selection + .map(|path| { + path.into_path() + .map(|path| path.to_string_lossy().into_owned()) + .map_err(|error| format!("The selected model directory is invalid: {error}")) + }) + .transpose() +} + +#[tauri::command] +async fn install_media_runtime( + app: AppHandle, + state: tauri::State<'_, DesktopState>, +) -> Result { + let current = inspect_media_runtime(); + if current.ready { + return Ok(current); + } + if state + .operation_active + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return Err("Another install or model-preparation operation is active.".into()); + } + let _operation_guard = OperationGuard { + active: &state.operation_active, + }; + let plan = system_install_plan().ok_or("No supported system package manager was found.")?; + if !plan.automatic { + let instruction = format!( + "VidXP needs FFmpeg and ffprobe.\n\nRun this command in a terminal, then return to VidXP:\n\n{}", + display_command(&plan.command) + ); + app.dialog() + .message(&instruction) + .title("FFmpeg is required") + .kind(MessageDialogKind::Warning) + .blocking_show(); + return Err(instruction); + } + let approved = app + .dialog() + .message(format!( + "VidXP needs FFmpeg and ffprobe for video processing.\n\nInstall them with {}?\n\n{}", + plan.manager, + display_command(&plan.command) + )) + .title("Install FFmpeg") + .kind(MessageDialogKind::Info) + .buttons(MessageDialogButtons::OkCancelCustom( + "Install".into(), + "Not now".into(), + )) + .blocking_show(); + if !approved { + return Err("FFmpeg setup was deferred.".into()); + } + let command = app + .shell() + .command(plan.command[0].clone()) + .args(&plan.command[1..]); + supervised_output( + &state, + command, + &format!("{} FFmpeg installation", plan.manager), + ) + .await?; + let status = inspect_media_runtime(); + if !status.ready { + return Err(format!( + "FFmpeg installation finished but verification failed: {}", + status.errors.join(" ") + )); + } + Ok(status) +} + +#[tauri::command] +fn runtime_status(app: AppHandle) -> Result { + let manifest = manifest()?; + let mut paths = desktop_paths(&app)?; + let default_model_directory = paths.models.to_string_lossy().into_owned(); + if let Err(detail) = verified_media_runtime() { + return Ok(RuntimeStatus { + ready: false, + package_version: manifest.package_version, + capabilities: Vec::new(), + surfaces: Vec::new(), + model_directory: default_model_directory, + detail, + }); + } + let active = match active_runtime(&paths) { + Ok(active) => active, + Err(detail) => { + return Ok(RuntimeStatus { + ready: false, + package_version: manifest.package_version, + capabilities: Vec::new(), + surfaces: Vec::new(), + model_directory: default_model_directory, + detail, + }); + } + }; + paths.models = active.model_directory.clone(); + let runtime = runtime_directory(&paths, &active); + let version = run_vidxp( + &runtime, + &paths, + &["--version".into()], + "VidXP runtime validation", + ) + .and_then(|output| { + let actual = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + let expected = format!("VidXP {}", manifest.package_version); + if actual == expected { + Ok(output) + } else { + Err(format!( + "The active runtime reported {actual:?}; expected {expected:?}." + )) + } + }); + Ok(RuntimeStatus { + ready: version.is_ok(), + package_version: active.package_version, + capabilities: active.capabilities, + surfaces: active.surfaces, + model_directory: active.model_directory.to_string_lossy().into_owned(), + detail: version + .err() + .unwrap_or_else(|| "Local video processing is ready.".into()), + }) +} + +#[tauri::command] +async fn install_runtime( + app: AppHandle, + state: tauri::State<'_, DesktopState>, + request: InstallRequest, +) -> Result { + if state + .operation_active + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return Err("Another install or model-preparation operation is active.".into()); + } + let _operation_guard = OperationGuard { + active: &state.operation_active, + }; + let manifest = manifest()?; + let capabilities = selected_capabilities(&manifest, &request.capabilities)?; + let surfaces = selected_surfaces(&manifest, &request.surfaces)?; + let media_runtime = verified_media_runtime()?; + let mut paths = desktop_paths(&app)?; + paths.models = model_directory(&paths, request.model_directory.as_deref())?; + for directory in [ + &paths.data, + &paths.cache, + &paths.repository, + &paths.runtimes, + &paths.python, + &paths.models, + ] { + fs::create_dir_all(directory) + .map_err(|error| format!("Could not create {}: {error}", directory.display()))?; + } + + let profile_seed = format!( + "{}:{}:{}:{}:{}", + manifest_digest(), + std::env::consts::OS, + std::env::consts::ARCH, + capabilities.join(","), + surfaces.join(",") + ); + let profile_hash = hex::encode(Sha256::digest(profile_seed.as_bytes())); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| format!("The system clock is invalid: {error}"))? + .as_nanos(); + let staging_name = format!(".staging-{profile_hash}-{timestamp}-{}", std::process::id()); + let staging = paths.runtimes.join(&staging_name); + let constraints = staging.join("runtime-constraints.txt"); + + let install_result = async { + uv_output( + &app, + &state, + &paths, + vec![ + "venv".into(), + staging.to_string_lossy().into_owned(), + "--python".into(), + manifest.python_version.clone(), + "--managed-python".into(), + "--no-config".into(), + ], + "Managed Python setup", + ) + .await?; + + fs::write(&constraints, normalized_runtime_constraints().as_ref()) + .map_err(|error| format!("Could not write runtime constraints: {error}"))?; + + uv_output( + &app, + &state, + &paths, + package_acquisition_arguments(&manifest, &executable(&staging, "python")), + "VidXP package acquisition", + ) + .await?; + + uv_output( + &app, + &state, + &paths, + dependency_installation_arguments( + &manifest, + &capabilities, + &surfaces, + &executable(&staging, "python"), + &constraints, + !cfg!(target_os = "macos"), + ), + "VidXP package installation", + ) + .await?; + + run_vidxp_supervised( + &app, + &state, + &staging, + &paths, + &[ + "init".into(), + "--json".into(), + "--ffmpeg".into(), + media_runtime.ffmpeg.to_string_lossy().into_owned(), + "--ffprobe".into(), + media_runtime.ffprobe.to_string_lossy().into_owned(), + ], + "FFmpeg configuration", + ) + .await?; + + let doctor_arguments = capability_command_arguments(&manifest, "doctor", &capabilities); + run_vidxp_supervised( + &app, + &state, + &staging, + &paths, + &doctor_arguments, + "VidXP dependency validation", + ) + .await?; + + if request.prepare_models { + let prepare_arguments = + capability_command_arguments(&manifest, "prepare", &capabilities); + *state + .operation_worker_runtime + .lock() + .map_err(|_| "The preparation worker supervisor is unavailable.")? = + Some(staging.clone()); + let preparation = run_vidxp_supervised( + &app, + &state, + &staging, + &paths, + &prepare_arguments, + "VidXP model preparation", + ) + .await; + let _ = run_vidxp( + &staging, + &paths, + &["jobs".into(), "stop-worker".into()], + "VidXP preparation worker shutdown", + ); + if let Ok(mut worker_runtime) = state.operation_worker_runtime.lock() { + worker_runtime.take(); + } + preparation?; + } + + Ok::<(), String>(()) + } + .await; + if let Err(error) = install_result { + let cleanup_error = if staging.exists() { + fs::remove_dir_all(&staging).err() + } else { + None + }; + return Err(match cleanup_error { + Some(cleanup_error) => format!( + "{error}. The previous active runtime was not changed. VidXP could not remove the failed staged runtime at {}: {cleanup_error}", + staging.display() + ), + None => format!( + "{error}. The previous active runtime was not changed, and the failed staged runtime was removed." + ), + }); + } + + let profile = format!("{profile_hash}-{timestamp}"); + let runtime = paths.runtimes.join(&profile); + fs::rename(&staging, &runtime) + .map_err(|error| format!("Could not finalize the validated runtime: {error}"))?; + let active = ActiveRuntime { + schema_version: 2, + manifest_sha256: manifest_digest(), + profile, + package_version: manifest.package_version.clone(), + capabilities: capabilities.clone(), + surfaces: surfaces.clone(), + model_directory: paths.models.clone(), + }; + write_active_runtime(&paths, &active)?; + + Ok(InstallResult { + package_version: manifest.package_version, + capabilities, + surfaces, + model_directory: paths.models.to_string_lossy().into_owned(), + prepared: request.prepare_models, + }) +} + +fn start_ui(app: &AppHandle, state: &DesktopState) -> Result { + let mut paths = desktop_paths(&app)?; + let active = active_runtime(&paths)?; + if !active.surfaces.iter().any(|surface| surface == "browser") { + return Err( + "The browser interface is not installed. Reconfigure VidXP and select Browser interface." + .into(), + ); + } + paths.models = active.model_directory.clone(); + let runtime = runtime_directory(&paths, &active); + let mut active_process = state + .ui_process + .lock() + .map_err(|_| "The desktop process supervisor is unavailable.".to_string())?; + if let Some(ui) = active_process.as_mut() { + if ui + .process + .try_wait() + .map_err(|error| format!("Could not inspect the interface process: {error}"))? + .is_none() + { + return Ok(ui.url.clone()); + } + *active_process = None; + } + + let listener = TcpListener::bind(("127.0.0.1", 0)) + .map_err(|error| format!("Could not reserve a local interface port: {error}"))?; + let port = listener + .local_addr() + .map_err(|error| format!("Could not identify the local interface port: {error}"))? + .port(); + drop(listener); + + let mut command = configured_command(&executable(&runtime, "vidxp"), &paths); + command + .arg("--index-dir") + .arg(&paths.repository) + .args(["ui", "--host", "127.0.0.1", "--port", &port.to_string()]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + let mut process = command + .spawn() + .map_err(|error| format!("Could not start the VidXP interface: {error}"))?; + + let address = SocketAddr::from(([127, 0, 0, 1], port)); + let deadline = Instant::now() + Duration::from_secs(30); + while Instant::now() < deadline { + if let Some(status) = process + .try_wait() + .map_err(|error| format!("Could not inspect the interface process: {error}"))? + { + return Err(format!( + "The VidXP interface exited during startup ({status})." + )); + } + if TcpStream::connect_timeout(&address, Duration::from_millis(100)).is_ok() { + let url = format!("http://127.0.0.1:{port}"); + *active_process = Some(ManagedUi { + process, + url: url.clone(), + }); + return Ok(url); + } + thread::sleep(Duration::from_millis(100)); + } + let _ = process.kill(); + let _ = process.wait(); + Err("The VidXP interface did not become ready in 30 seconds.".into()) +} + +fn hide_main_window(app: &AppHandle) -> Result<(), String> { + let window = app + .get_webview_window("main") + .ok_or("The VidXP desktop window is unavailable.")?; + window + .hide() + .map_err(|error| format!("Could not hide VidXP to the system tray: {error}")) +} + +fn show_main_window(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } +} + +fn configured_runtime(app: &AppHandle) -> bool { + desktop_paths(app) + .and_then(|paths| active_runtime(&paths)) + .is_ok() +} + +fn browser_surface_configured(app: &AppHandle) -> bool { + desktop_paths(app) + .and_then(|paths| active_runtime(&paths)) + .is_ok_and(|active| active.surfaces.iter().any(|surface| surface == "browser")) +} + +fn open_ui_in_browser(app: &AppHandle, state: &DesktopState) -> Result<(), String> { + let url = start_ui(app, state)?; + app.opener() + .open_url(&url, None::<&str>) + .map_err(|error| format!("Could not open VidXP in the default browser: {error}"))?; + hide_main_window(app) +} + +fn open_or_show(app: &AppHandle) { + if !browser_surface_configured(app) { + show_main_window(app); + return; + } + let app = app.clone(); + thread::spawn(move || { + let state = app.state::(); + if let Err(error) = open_ui_in_browser(&app, &state) { + show_main_window(&app); + app.dialog() + .message(error) + .title("VidXP could not open") + .kind(MessageDialogKind::Error) + .blocking_show(); + } + }); +} + +fn begin_shutdown(app: &AppHandle) { + if app + .state::() + .shutdown_started + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return; + } + log::info!("VidXP supervised shutdown requested"); + shutdown(app); + log::info!("VidXP supervised shutdown completed"); + std::process::exit(0); +} + +#[tauri::command] +fn launch_ui(app: AppHandle, state: tauri::State<'_, DesktopState>) -> Result<(), String> { + open_ui_in_browser(&app, &state) +} + +#[tauri::command] +fn hide_to_tray(app: AppHandle) -> Result<(), String> { + if !configured_runtime(&app) { + return Err("Local video processing has not been configured yet.".into()); + } + hide_main_window(&app) +} + +fn create_tray(app: &tauri::App) -> tauri::Result<()> { + let open = MenuItem::with_id(app, "open", "Open VidXP", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit VidXP", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&open, &quit])?; + let mut tray = TrayIconBuilder::with_id("vidxp") + .tooltip("VidXP") + .menu(&menu) + .show_menu_on_left_click(true) + .on_menu_event(|app, event| match event.id().as_ref() { + "open" => open_or_show(app), + "quit" => begin_shutdown(app), + _ => {} + }); + if let Some(icon) = app.default_window_icon() { + tray = tray.icon(icon.clone()); + } + tray.build(app)?; + Ok(()) +} + +fn stop_worker(runtime: &Path, paths: &DesktopPaths) { + let mut command = configured_command(&executable(runtime, "vidxp"), paths); + command + .arg("--index-dir") + .arg(&paths.repository) + .args(["jobs", "stop-worker"]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + let Ok(mut process) = command.spawn() else { + return; + }; + match process.wait_timeout(Duration::from_secs(5)) { + Ok(Some(_)) => {} + _ => { + let _ = process.kill(); + let _ = process.wait_timeout(Duration::from_secs(1)); + } + } +} + +fn shutdown(app: &AppHandle) { + log::info!("Stopping active VidXP processes"); + let state = app.state::(); + if let Ok(mut active_operation) = state.operation_process.lock() { + if let Some(process) = active_operation.take() { + let _ = process.kill(); + } + } + if let Ok(mut active_process) = state.ui_process.lock() { + if let Some(mut ui) = active_process.take() { + let _ = ui.process.kill(); + match ui.process.wait_timeout(Duration::from_secs(5)) { + Ok(Some(_)) => {} + _ => { + let _ = ui.process.kill(); + let _ = ui.process.wait_timeout(Duration::from_secs(1)); + } + } + } + } + let Ok(mut paths) = desktop_paths(app) else { + log::warn!("Could not resolve desktop paths during shutdown"); + return; + }; + if let Ok(mut operation_worker) = state.operation_worker_runtime.lock() { + if let Some(runtime) = operation_worker.take() { + stop_worker(&runtime, &paths); + } + } + let Ok(active) = active_runtime(&paths) else { + log::info!("No active VidXP runtime needs worker shutdown"); + return; + }; + paths.models = active.model_directory.clone(); + let runtime = runtime_directory(&paths, &active); + stop_worker(&runtime, &paths); + log::info!("Active VidXP worker shutdown finished"); +} + +pub fn run() { + let builder = tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _, _| { + open_or_show(app); + })) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_log::Builder::new().build()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_shell::init()) + .manage(DesktopState::default()) + .setup(|app| { + migrate_legacy_shared_data(app.handle()).map_err(io::Error::other)?; + create_tray(app)?; + if !configured_runtime(app.handle()) { + show_main_window(app.handle()); + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + runtime_manifest, + media_runtime_status, + choose_model_directory, + install_media_runtime, + runtime_status, + install_runtime, + launch_ui, + hide_to_tray + ]); + let app = builder + .build(tauri::generate_context!()) + .expect("could not initialize VidXP desktop"); + app.run(|app_handle, event| match event { + RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { api, .. }, + .. + } if label == "main" => { + if app_handle + .state::() + .shutdown_started + .load(Ordering::Acquire) + { + return; + } + api.prevent_close(); + if configured_runtime(app_handle) { + let _ = hide_main_window(app_handle); + } else { + begin_shutdown(app_handle); + } + } + RunEvent::ExitRequested { api, .. } + if !app_handle + .state::() + .shutdown_started + .load(Ordering::Acquire) => + { + api.prevent_exit(); + begin_shutdown(app_handle); + } + _ => {} + }); +} + +#[cfg(test)] +mod tests { + use super::{ + base_package_specification, capability_command_arguments, + dependency_installation_arguments, desktop_paths_from_roots, display_command, manifest, + normalize_line_endings, normalized_runtime_constraints, package_acquisition_arguments, + package_index, package_specification, required_encoder_missing, selected_capabilities, + selected_surfaces, + }; + use std::path::{Path, PathBuf}; + + #[test] + fn desktop_runtime_is_private_while_product_data_is_shared() { + let paths = + desktop_paths_from_roots(Path::new("private"), Path::new("cache"), Path::new("local")); + + assert_eq!(paths.data, PathBuf::from("local").join("VidXP")); + assert_eq!( + paths.repository, + PathBuf::from("local") + .join("VidXP") + .join("repositories") + .join("default") + ); + assert_eq!( + paths.models, + PathBuf::from("local").join("VidXP").join("models") + ); + assert_eq!(paths.runtimes, PathBuf::from("private").join("runtimes")); + assert_eq!(paths.python, PathBuf::from("private").join("python")); + assert_eq!( + paths.active_runtime, + PathBuf::from("private").join("active-runtime.json") + ); + } + + #[test] + fn capability_selection_is_sorted_and_rejects_unknown_values() { + let manifest = manifest().expect("manifest"); + let selected = selected_capabilities( + &manifest, + &["scene".into(), "dialogue".into(), "scene".into()], + ) + .expect("selection"); + + assert_eq!(selected, ["dialogue", "scene"]); + assert!(selected_capabilities(&manifest, &["other".into()]).is_err()); + } + + #[test] + fn runtime_constraints_digest_is_independent_of_checkout_line_endings() { + let canonical = normalized_runtime_constraints(); + let windows = canonical + .split(|byte| *byte == b'\n') + .collect::>() + .join(&b"\r\n"[..]); + + assert_eq!( + normalize_line_endings(&windows).as_ref(), + canonical.as_ref() + ); + } + + #[test] + fn package_specification_has_one_sorted_extra_set() { + let manifest = manifest().expect("manifest"); + + assert_eq!(base_package_specification(&manifest), "vidxp==0.2.1-b.1"); + assert_eq!( + package_specification( + &manifest, + &["scene".into(), "dialogue".into()], + &["browser".into()], + ), + "vidxp[dialogue,frontend,scene]==0.2.1-b.1" + ); + assert_eq!( + package_specification(&manifest, &["scene".into()], &[]), + "vidxp[scene]==0.2.1-b.1" + ); + assert_eq!( + selected_surfaces(&manifest, &["browser".into(), "browser".into()]) + .expect("surface selection"), + ["browser"] + ); + assert!(selected_surfaces(&manifest, &["unknown".into()]).is_err()); + } + + #[test] + fn prerelease_package_and_dependencies_use_separate_indexes() { + let manifest = manifest().expect("manifest"); + let python = Path::new("managed-python"); + let constraints = Path::new("runtime-constraints.txt"); + let acquisition = package_acquisition_arguments(&manifest, python); + let dependencies = dependency_installation_arguments( + &manifest, + &["scene".into()], + &[], + python, + constraints, + true, + ); + + assert_eq!( + package_index(&manifest.package_version), + "https://test.pypi.org/simple" + ); + assert_eq!(package_index("0.3.0"), "https://pypi.org/simple"); + assert_eq!(manifest.dependency_index, "https://pypi.org/simple"); + assert!(acquisition.iter().any(|item| item == "--no-deps")); + assert!( + acquisition + .iter() + .any(|item| item == "https://test.pypi.org/simple") + ); + assert!(!dependencies.iter().any(|item| item == "--no-deps")); + assert!( + dependencies + .iter() + .any(|item| item == &manifest.dependency_index) + ); + assert!(dependencies.iter().any(|item| item == "--torch-backend")); + assert!( + dependencies + .windows(2) + .any(|items| items == ["--constraints", "runtime-constraints.txt"]) + ); + } + + #[test] + fn capability_commands_pass_one_comma_separated_option() { + let manifest = manifest().expect("manifest"); + + assert_eq!( + capability_command_arguments(&manifest, "doctor", &["dialogue".into(), "scene".into()]), + ["doctor", "--json", "--modalities", "dialogue,scene"] + ); + } + + #[test] + fn ffmpeg_encoder_check_matches_complete_encoder_names() { + let encoders = " V....D libx264 H.264\n A....D aac AAC"; + + assert!(!required_encoder_missing(encoders, "libx264")); + assert!(!required_encoder_missing(encoders, "aac")); + assert!(required_encoder_missing(encoders, "libx265")); + } + + #[test] + fn package_manager_command_is_presented_as_copyable_text() { + assert_eq!( + display_command(&[ + "winget".into(), + "install".into(), + "--id".into(), + "Gyan.FFmpeg".into(), + ]), + "winget install --id Gyan.FFmpeg" + ); + } +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..03c28bd --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + vidxp_desktop_lib::run(); +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..208ff38 --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "VidXP", + "version": "0.2.1-b.1", + "identifier": "dev.grayhat.vidxp", + "build": { + "frontendDist": "../web" + }, + "app": { + "withGlobalTauri": true, + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src ipc: http://ipc.localhost" + }, + "windows": [ + { + "label": "main", + "title": "VidXP", + "width": 920, + "height": 760, + "minWidth": 680, + "minHeight": 600, + "visible": false, + "resizable": true + } + ] + }, + "bundle": { + "active": true, + "targets": "all", + "externalBin": [ + "binaries/uv" + ], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "windows": { + "nsis": { + "installMode": "currentUser", + "installerHooks": "nsis-hooks.nsh" + } + }, + "macOS": { + "minimumSystemVersion": "13.0" + } + } +} diff --git a/desktop/web/app.js b/desktop/web/app.js new file mode 100644 index 0000000..7987bb0 --- /dev/null +++ b/desktop/web/app.js @@ -0,0 +1,188 @@ +const invoke = window.__TAURI__.core.invoke; + +const surfacesNode = document.querySelector("#surfaces"); +const capabilitiesNode = document.querySelector("#capabilities"); +const installButton = document.querySelector("#install"); +const launchButton = document.querySelector("#launch"); +const prepareModels = document.querySelector("#prepare-models"); +const modelDirectoryNode = document.querySelector("#model-directory"); +const chooseModelDirectoryButton = document.querySelector( + "#choose-model-directory", +); +const statusNode = document.querySelector("#status"); + +let manifest; +let selectedModelDirectory; + +function setBusy(busy) { + installButton.disabled = busy; + chooseModelDirectoryButton.disabled = busy; + launchButton.disabled = busy || launchButton.dataset.ready !== "true"; +} + +function selectedCapabilities() { + return [...document.querySelectorAll("[data-capability]:checked")].map( + (node) => node.dataset.capability, + ); +} + +function selectedSurfaces() { + return [...document.querySelectorAll("[data-surface]:checked")].map( + (node) => node.dataset.surface, + ); +} + +function renderOptions(entries, container, dataAttribute, className) { + container.replaceChildren(); + Object.entries(entries).forEach(([id, option]) => { + const label = document.createElement("label"); + label.className = className; + + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = option.default ?? true; + input.dataset[dataAttribute] = id; + + const copy = document.createElement("span"); + const name = document.createElement("strong"); + name.textContent = option.label; + const detail = document.createElement("small"); + detail.textContent = + option.description ?? `Installs the ${option.extra} capability`; + copy.append(name, detail); + label.append(input, copy); + container.append(label); + }); +} + +function renderManifest() { + renderOptions(manifest.surfaces, surfacesNode, "surface", "surface"); + renderOptions( + manifest.capabilities, + capabilitiesNode, + "capability", + "capability", + ); +} + +function applySelection(attribute, selected) { + document.querySelectorAll(`[data-${attribute}]`).forEach((node) => { + node.checked = selected.includes(node.dataset[attribute]); + }); +} + +async function refreshStatus() { + const status = await invoke("runtime_status"); + selectedModelDirectory ??= status.model_directory; + modelDirectoryNode.textContent = selectedModelDirectory; + + if (status.ready) { + applySelection("capability", status.capabilities); + applySelection("surface", status.surfaces); + } + + const browserReady = status.ready && status.surfaces.includes("browser"); + launchButton.dataset.ready = String(browserReady); + launchButton.disabled = !browserReady; + statusNode.textContent = status.ready + ? `Ready · VidXP ${status.package_version} · ${status.capabilities.join(", ")}` + : status.detail; + return status; +} + +async function ensureMediaRuntime() { + const status = await invoke("media_runtime_status"); + if (status.ready) { + return status; + } + if (!status.install_command) { + throw new Error( + `${status.errors.join(" ")} Install FFmpeg and ffprobe, then retry.`, + ); + } + statusNode.textContent = "Waiting for FFmpeg setup confirmation…"; + return invoke("install_media_runtime"); +} + +async function launchBrowser() { + setBusy(true); + statusNode.textContent = "Starting VidXP…"; + try { + await invoke("launch_ui"); + statusNode.textContent = "VidXP is running in the system tray."; + } catch (error) { + statusNode.textContent = `Launch failed: ${error}`; + setBusy(false); + } +} + +chooseModelDirectoryButton.addEventListener("click", async () => { + try { + const selection = await invoke("choose_model_directory"); + if (selection) { + selectedModelDirectory = selection; + modelDirectoryNode.textContent = selection; + } + } catch (error) { + statusNode.textContent = `Could not select the model folder: ${error}`; + } +}); + +installButton.addEventListener("click", async () => { + const capabilities = selectedCapabilities(); + if (capabilities.length === 0) { + statusNode.textContent = "Select at least one capability."; + return; + } + + setBusy(true); + try { + statusNode.textContent = "Checking FFmpeg and required codecs…"; + await ensureMediaRuntime(); + statusNode.textContent = prepareModels.checked + ? "Configuring local processing and downloading selected models…" + : "Configuring local processing…"; + const result = await invoke("install_runtime", { + request: { + capabilities, + surfaces: selectedSurfaces(), + prepare_models: prepareModels.checked, + model_directory: selectedModelDirectory, + }, + }); + selectedModelDirectory = result.model_directory; + statusNode.textContent = result.prepared + ? "Local processing and models are ready." + : "Local processing is ready. Model downloads were deferred."; + await refreshStatus(); + if (result.surfaces.includes("browser")) { + await launchBrowser(); + } else { + await invoke("hide_to_tray"); + } + } catch (error) { + statusNode.textContent = `Configuration failed: ${error}`; + } finally { + setBusy(false); + } +}); + +launchButton.addEventListener("click", launchBrowser); + +async function start() { + try { + manifest = await invoke("runtime_manifest"); + renderManifest(); + const status = await refreshStatus(); + if (status.ready && status.surfaces.includes("browser")) { + await launchBrowser(); + } else if (status.ready) { + await invoke("hide_to_tray"); + } + } catch (error) { + statusNode.textContent = `Desktop initialization failed: ${error}`; + setBusy(true); + } +} + +start(); diff --git a/desktop/web/index.html b/desktop/web/index.html new file mode 100644 index 0000000..ecc5848 --- /dev/null +++ b/desktop/web/index.html @@ -0,0 +1,73 @@ + + + + + + + VidXP + + + + +

    +
    +

    VIDXP DESKTOP

    +

    Configure local video processing.

    +

    + VidXP is installed. Choose what local processing it should prepare on + this machine. +

    +
    + +
    +

    Interface

    +
    +
    + +
    +

    Capabilities

    +
    +
    + +
    +

    Models

    +
    +
    + Model storage + Using the default location +
    + +
    + +
    + +
    +
    +

    STATUS

    +

    Checking the local runtime…

    +
    +
    + + +
    +
    + +

    + Configuration uses an app-owned Python runtime. VidXP checks FFmpeg, + ffprobe, and required codecs first and uses operating-system prompts + before running a supported package manager. +

    +
    + + + diff --git a/desktop/web/styles.css b/desktop/web/styles.css new file mode 100644 index 0000000..61ad67e --- /dev/null +++ b/desktop/web/styles.css @@ -0,0 +1,211 @@ +:root { + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + color: #eceef3; + background: #0d1016; + font-synthesis: none; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at 80% 0%, rgba(86, 100, 255, 0.2), transparent 36rem), + #0d1016; +} + +main { + width: min(48rem, calc(100% - 3rem)); + margin: 0 auto; + padding: 4rem 0; +} + +.hero { + margin-bottom: 2rem; +} + +.eyebrow { + margin: 0 0 0.55rem; + color: #9da8ff; + font-size: 0.72rem; + font-weight: 750; + letter-spacing: 0.14em; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1 { + max-width: 13ch; + margin-bottom: 1rem; + font-size: clamp(2.4rem, 7vw, 4.5rem); + line-height: 0.98; + letter-spacing: -0.055em; +} + +h2 { + font-size: 1rem; +} + +.lede, +.footnote, +small { + color: #abb1be; +} + +.lede { + max-width: 39rem; + font-size: 1.05rem; + line-height: 1.65; +} + +.panel { + margin-top: 1rem; + padding: 1.35rem; + border: 1px solid #282e3b; + border-radius: 1rem; + background: rgba(21, 25, 34, 0.9); +} + +.capabilities, +.options { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; +} + +.capability, +.surface, +.prepare-option { + display: flex; + gap: 0.7rem; + align-items: flex-start; + cursor: pointer; +} + +.capability { + min-height: 5rem; + padding: 0.9rem; + border: 1px solid #303746; + border-radius: 0.75rem; + background: #171c26; +} + +.surface { + padding: 0.9rem; + border: 1px solid #303746; + border-radius: 0.75rem; + background: #171c26; +} + +.capability:has(input:checked), +.surface:has(input:checked) { + border-color: #7d88ff; + background: #1d2340; +} + +input { + margin-top: 0.2rem; + accent-color: #7d88ff; +} + +.capability span, +.surface span, +.prepare-option span { + display: grid; + gap: 0.3rem; +} + +.path-option { + display: flex; + gap: 1rem; + align-items: center; + justify-content: space-between; +} + +.path-option div { + display: grid; + min-width: 0; + gap: 0.35rem; +} + +.path-option small { + overflow-wrap: anywhere; +} + +.prepare-option { + margin-top: 1.1rem; + padding-top: 1.1rem; + border-top: 1px solid #282e3b; +} + +.status-panel, +.actions { + display: flex; + gap: 1rem; + align-items: center; + justify-content: space-between; +} + +.status-panel p:last-child { + margin-bottom: 0; +} + +.actions { + flex-shrink: 0; +} + +button { + min-height: 2.7rem; + padding: 0 1rem; + border: 0; + border-radius: 0.65rem; + color: #0d1016; + background: #a9b0ff; + font: inherit; + font-weight: 720; + cursor: pointer; +} + +button.secondary { + color: #eceef3; + background: #303746; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.footnote { + margin: 1rem 0 0; + font-size: 0.78rem; + line-height: 1.5; +} + +@media (max-width: 42rem) { + main { + width: min(100% - 2rem, 48rem); + padding: 2rem 0; + } + + .capabilities, + .options { + grid-template-columns: 1fr; + } + + .status-panel, + .path-option, + .actions { + align-items: stretch; + flex-direction: column; + } +} From 600e3335bfd5b0dd97406b4c66dbb4b291db2049 Mon Sep 17 00:00:00 2001 From: Talha Date: Fri, 31 Jul 2026 00:21:59 +0500 Subject: [PATCH 4/8] feat(deploy): add published server deployment profiles --- Dockerfile | 108 +++++++-------- compose.coolify.yaml | 214 ++++++++++++++++++++++++++++++ docs/deployment/coolify.md | 138 +++++++++++++++++++ docs/deployment/gpu-evaluation.md | 136 +++++++++++++++++++ 4 files changed, 537 insertions(+), 59 deletions(-) create mode 100644 compose.coolify.yaml create mode 100644 docs/deployment/coolify.md create mode 100644 docs/deployment/gpu-evaluation.md diff --git a/Dockerfile b/Dockerfile index e74c2f4..75c0d46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,93 +1,83 @@ # syntax=docker/dockerfile:1 -FROM python:3.11-slim-bookworm AS builder +FROM python:3.14-slim-trixie AS build-base WORKDIR /app -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - cmake \ - git \ - pkg-config \ - libopenblas-dev \ - liblapack-dev \ - libjpeg62-turbo-dev \ - libpng-dev \ - && rm -rf /var/lib/apt/lists/* - -RUN python -m venv /opt/vidxp +RUN python -m pip install --no-cache-dir "uv==0.12.0" -ENV PATH="/opt/vidxp/bin:${PATH}" \ - PIP_DISABLE_PIP_VERSION_CHECK=1 - -COPY pyproject.toml README.md LICENSE MANIFEST.in ./ -COPY src/vidxp/requirements ./src/vidxp/requirements -COPY src/vidxp/capabilities/dialogue/requirements.txt ./src/vidxp/capabilities/dialogue/requirements.txt -COPY src/vidxp/capabilities/scene/requirements.txt ./src/vidxp/capabilities/scene/requirements.txt -COPY src/vidxp/capabilities/actor/requirements.txt ./src/vidxp/capabilities/actor/requirements.txt - -ARG PYTORCH_INDEX_URL="https://download.pytorch.org/whl/cpu" - -RUN --mount=type=cache,target=/root/.cache/pip \ - python -m pip install --upgrade pip setuptools wheel \ - && python -m pip install \ - --extra-index-url "${PYTORCH_INDEX_URL}" \ - -r src/vidxp/requirements/storage.txt \ - -r src/vidxp/requirements/frontend.txt \ - -r src/vidxp/capabilities/dialogue/requirements.txt \ - -r src/vidxp/capabilities/scene/requirements.txt \ - -r src/vidxp/capabilities/actor/requirements.txt +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 +COPY pyproject.toml uv.lock README.md LICENSE MANIFEST.in ./ COPY src ./src -RUN --mount=type=cache,target=/root/.cache/pip \ - python -m pip install . \ - && python -m pip check +FROM build-base AS local-builder +ENV UV_PROJECT_ENVIRONMENT="/opt/vidxp" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --extra local-worker --extra frontend \ + --no-dev --no-editable \ + && uv pip check --python /opt/vidxp/bin/python + +FROM build-base AS control-builder +ENV UV_PROJECT_ENVIRONMENT="/opt/vidxp" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --extra server --no-dev --no-editable \ + && uv pip check --python /opt/vidxp/bin/python + +FROM build-base AS worker-builder +ENV UV_PROJECT_ENVIRONMENT="/opt/vidxp" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --extra server-worker \ + --no-dev --no-editable \ + && uv pip check --python /opt/vidxp/bin/python -FROM python:3.11-slim-bookworm AS runtime +FROM python:3.14-slim-trixie AS runtime-base LABEL org.opencontainers.image.title="VidXP" \ - org.opencontainers.image.description="Local-first video indexing and search" \ + org.opencontainers.image.description="Video indexing and search" \ org.opencontainers.image.source="https://github.com/grayhatdevelopers/vidxp" \ org.opencontainers.image.licenses="MIT" RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ libglib2.0-0 \ - libgl1 \ libgomp1 \ - libjpeg62-turbo \ - liblapack3 \ - libopenblas0 \ - libpng16-16 \ - libsm6 \ - libxext6 \ - libxrender1 \ && rm -rf /var/lib/apt/lists/* \ - && groupadd --system vidxp \ - && useradd --system --gid vidxp --home-dir /var/lib/vidxp vidxp \ + && groupadd --system --gid 1000 vidxp \ + && useradd --system --uid 1000 --gid vidxp \ + --home-dir /var/lib/vidxp vidxp \ && mkdir -p /var/lib/vidxp \ && chown vidxp:vidxp /var/lib/vidxp -COPY --from=builder /opt/vidxp /opt/vidxp - ENV PATH="/opt/vidxp/bin:${PATH}" \ - HOME="/var/lib/vidxp" \ PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \ - STREAMLIT_SERVER_HEADLESS=true \ - VIDXP_CONFIG_FILE="/var/lib/vidxp/config/repositories.json" \ - VIDXP_INDEX_DIR="/var/lib/vidxp/index" + PYTHONUNBUFFERED=1 USER vidxp WORKDIR /var/lib/vidxp +FROM runtime-base AS control +COPY --from=control-builder /opt/vidxp /opt/vidxp +EXPOSE 8000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=3)"] +CMD ["vidxp-api"] + +FROM runtime-base AS worker +COPY --from=worker-builder /opt/vidxp /opt/vidxp +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD ["python", "-m", "vidxp.health_cli", "worker"] +CMD ["vidxp-worker", "--role", "cpu"] + +FROM runtime-base AS local +COPY --from=local-builder /opt/vidxp /opt/vidxp +ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \ + STREAMLIT_SERVER_HEADLESS=true \ + VIDXP_CONFIG_FILE="/var/lib/vidxp/config/repositories.json" \ + VIDXP_INDEX_DIR="/var/lib/vidxp/index" VOLUME ["/var/lib/vidxp"] - EXPOSE 8501 - HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8501/_stcore/health', timeout=3)"] - CMD ["vidxp", "ui", "--host", "0.0.0.0", "--port", "8501"] diff --git a/compose.coolify.yaml b/compose.coolify.yaml new file mode 100644 index 0000000..cd6d601 --- /dev/null +++ b/compose.coolify.yaml @@ -0,0 +1,214 @@ +x-vidxp-environment: &vidxp-environment + VIDXP_MODE: server + VIDXP_RUNTIME_BACKEND: cpu + VIDXP_REPOSITORY_ROOT: /var/lib/vidxp/data + VIDXP_MODEL_CACHE: /var/lib/vidxp/models + PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD} + VIDXP_HTTP_BIND_HOST: 0.0.0.0 + VIDXP_HTTP_AUTH_MODE: static + VIDXP_HTTP_STATIC_BEARER_TOKEN: ${VIDXP_HTTP_STATIC_BEARER_TOKEN:?Set a bearer token of at least 32 characters} + VIDXP_HTTP_TRUSTED_HOSTS: '["127.0.0.1","localhost","${VIDXP_PUBLIC_API_HOST:?Set the public API host}"]' + VIDXP_MCP_ALLOWED_HOSTS: '["127.0.0.1:*","localhost:*","${VIDXP_PUBLIC_API_HOST:?Set the public API host}"]' + VIDXP_UPLOAD_PUBLIC_ENDPOINT: ${VIDXP_UPLOAD_PUBLIC_ENDPOINT:?Set the public tusd endpoint ending in /uploads/} + VIDXP_UPLOAD_INTERNAL_ENDPOINT: http://tusd:8080/uploads/ + VIDXP_UPLOAD_CLEANUP_TOKEN: ${VIDXP_UPLOAD_CLEANUP_TOKEN:?Set a cleanup token of at least 32 characters} + VIDXP_UPLOAD_QUARANTINE_ROOT: /var/lib/vidxp/uploads + VIDXP_UPLOAD_MAX_BYTES: ${VIDXP_UPLOAD_MAX_BYTES:-53687091200} + VIDXP_UPLOAD_QUOTA_BYTES: ${VIDXP_UPLOAD_QUOTA_BYTES:-107374182400} + +services: + postgres: + image: postgres:18.3-trixie@sha256:7e32e9833a6fb1c92c32552794cb6ed569d51b445a54907d35fc112ef39684db + environment: + POSTGRES_DB: vidxp + POSTGRES_USER: vidxp + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vidxp -d vidxp"] + interval: 5s + timeout: 5s + retries: 20 + restart: unless-stopped + + chroma: + image: chromadb/chroma:1.5.9@sha256:1e0b73a187a28757c572acba508c46f48c9e8b0acaf5c20e6d95cdedce1acdf6 + volumes: + - chroma-data:/data + restart: unless-stopped + + storage-init: + image: ${VIDXP_CONTROL_IMAGE:?Set the immutable VidXP control image} + user: "0:0" + command: + - chown + - -R + - vidxp:vidxp + - /var/lib/vidxp/data + - /var/lib/vidxp/uploads + - /var/lib/vidxp/models + volumes: + - content-data:/var/lib/vidxp/data + - upload-quarantine:/var/lib/vidxp/uploads + - model-cache:/var/lib/vidxp/models + healthcheck: + disable: true + restart: "no" + + chroma-ready: + image: ${VIDXP_CONTROL_IMAGE:?Set the immutable VidXP control image} + command: ["python", "-m", "vidxp.health_cli", "chroma"] + depends_on: + chroma: + condition: service_started + healthcheck: + disable: true + restart: "no" + + migrate: + image: ${VIDXP_CONTROL_IMAGE:?Set the immutable VidXP control image} + environment: + <<: *vidxp-environment + command: + - sh + - -c + - >- + vidxp-database && + dbos migrate + --sys-db-url postgresql://vidxp@postgres:5432/vidxp + --schema dbos + depends_on: + postgres: + condition: service_healthy + storage-init: + condition: service_completed_successfully + healthcheck: + disable: true + restart: "no" + + api: + image: ${VIDXP_CONTROL_IMAGE:?Set the immutable VidXP control image} + environment: + <<: *vidxp-environment + command: ["vidxp-api"] + volumes: + - content-data:/var/lib/vidxp/data + depends_on: + migrate: + condition: service_completed_successfully + healthcheck: + test: + - CMD + - python + - -c + - "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready', timeout=5)" + interval: 15s + timeout: 10s + retries: 20 + restart: unless-stopped + + hooks: + image: ${VIDXP_CONTROL_IMAGE:?Set the immutable VidXP control image} + environment: + <<: *vidxp-environment + command: ["vidxp-hooks"] + volumes: + - upload-quarantine:/var/lib/vidxp/uploads:ro + depends_on: + migrate: + condition: service_completed_successfully + healthcheck: + test: + - CMD + - python + - -c + - "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)" + interval: 15s + timeout: 5s + retries: 20 + restart: unless-stopped + + worker: + image: ${VIDXP_WORKER_IMAGE:?Set the immutable VidXP worker image} + environment: + <<: *vidxp-environment + VIDXP_ALLOW_MODEL_DOWNLOADS: ${VIDXP_ALLOW_MODEL_DOWNLOADS:-true} + VIDXP_SLM_BASE_URL: ${VIDXP_SLM_BASE_URL:-} + VIDXP_SLM_MODEL: ${VIDXP_SLM_MODEL:-} + command: ["vidxp-worker", "--role", "cpu"] + volumes: + - content-data:/var/lib/vidxp/data + - upload-quarantine:/var/lib/vidxp/uploads:ro + - model-cache:/var/lib/vidxp/models + depends_on: + migrate: + condition: service_completed_successfully + chroma-ready: + condition: service_completed_successfully + stop_grace_period: 40s + restart: unless-stopped + + ollama: + image: ollama/ollama:0.32.5@sha256:4dea9fb511947e24a84237bb636b0203abcb2ff0d3fbc7b4ff865deb91362131 + profiles: ["slm"] + volumes: + - ollama-models:/root/.ollama + healthcheck: + test: ["CMD", "ollama", "list"] + interval: 10s + timeout: 5s + retries: 20 + restart: unless-stopped + + tusd: + image: tusproject/tusd:v2.10.0@sha256:9610f0f8edf4cceeb26f1b3ac9fa4bb4803d61a878e222c1573cda63c9e23374 + command: + - -host=0.0.0.0 + - -port=8080 + - -upload-dir=/srv/tusd-data + - -base-path=/uploads/ + - -behind-proxy + - -max-size=${VIDXP_UPLOAD_MAX_BYTES:-53687091200} + - -disable-download + - -disable-concatenation + - -hooks-http=http://hooks:8000/hooks + - -hooks-enabled-events=pre-create,post-finish,pre-terminate,post-terminate + - -hooks-http-timeout=5s + - -hooks-http-size-limit=5120 + - -hooks-http-retry=3 + - -hooks-http-backoff=1s + - -cors-allow-origin=${VIDXP_UPLOAD_CORS_ORIGIN_REGEX:?Set an anchored browser-origin regex} + - -cors-allow-headers=Authorization + - -cors-max-age=600 + - -network-timeout=60s + - -shutdown-timeout=30s + - -verbose=false + - -show-startup-logs=false + - -log-format=json + volumes: + - upload-quarantine:/srv/tusd-data + depends_on: + hooks: + condition: service_healthy + stop_grace_period: 35s + healthcheck: + test: + - CMD + - wget + - -q + - -O + - /dev/null + - http://127.0.0.1:8080/metrics + interval: 10s + timeout: 5s + retries: 20 + restart: unless-stopped + +volumes: + postgres-data: + chroma-data: + content-data: + upload-quarantine: + model-cache: + ollama-models: diff --git a/docs/deployment/coolify.md b/docs/deployment/coolify.md new file mode 100644 index 0000000..1113e8b --- /dev/null +++ b/docs/deployment/coolify.md @@ -0,0 +1,138 @@ +# Coolify deployment + +`compose.coolify.yaml` is a prebuilt-image Compose deployment. It has no build +contexts and does not download models in the API process. + +## Images + +- `VIDXP_CONTROL_IMAGE`: the VidXP `control` target for the API, private tusd + hooks, MCP server, readiness checks, and migrations. It contains no PyTorch or + Chroma server. +- `VIDXP_WORKER_IMAGE`: the VidXP `worker` target with CPU capabilities and + Chroma's HTTP-only client. +- PostgreSQL 18.3, Chroma 1.5.9, tusd 2.10.0, and the optional self-hosted + Ollama 0.32.5 service are pinned by digest in the Compose file. + +Use immutable VidXP release tags or digests for both first-party image variables. + +## Required variables + +```dotenv +VIDXP_CONTROL_IMAGE=ghcr.io/grayhatdevelopers/vidxp:-control +VIDXP_WORKER_IMAGE=ghcr.io/grayhatdevelopers/vidxp:-worker +POSTGRES_PASSWORD= +VIDXP_HTTP_STATIC_BEARER_TOKEN= +VIDXP_UPLOAD_CLEANUP_TOKEN= +VIDXP_PUBLIC_API_HOST=api.example.com +VIDXP_UPLOAD_PUBLIC_ENDPOINT=https://uploads.example.com/uploads/ +VIDXP_UPLOAD_CORS_ORIGIN_REGEX=^https://app\.example\.com$ +``` + +Optional deployment-wide upload limits use `VIDXP_UPLOAD_MAX_BYTES` for one object +and `VIDXP_UPLOAD_QUOTA_BYTES` for all reserved upload bytes in the singleton +repository. There is no per-principal or per-repository quota setting. + +Route the API service's port 8000 to the API hostname. Route only tusd's +`/uploads/` path on port 8080 to the upload hostname. Do not publish PostgreSQL, +Chroma, or the hook service. + +The same API origin exposes Streamable HTTP MCP at `/mcp`. Configure the proxy +to preserve `Authorization`, `Accept`, `Content-Type`, `MCP-Protocol-Version`, +`Mcp-Method`, `Mcp-Name`, and `Mcp-Param-*` headers and disable response buffering +for `/mcp`. Static bearer mode intentionally publishes no OAuth metadata; configure +the bearer header in the remote MCP client. + +The upload path is a capability URL used to resume an upload. Disable or redact +reverse-proxy access logging for `/uploads/`; VidXP cannot control logs written by +an upstream proxy. + +Video bytes do not travel through MCP. Create and resume the upload through the +HTTP/tus endpoints, wait for its `media_id`, and then use that ID with +`start_indexing`. + +## Prepare worker models + +Start the stack, then explicitly prepare the model set before accepting +indexing requests. Submit preparation through the authenticated API so the +durable job executes on the worker and writes to the shared model volume: + +| Selected models | Maximum pinned download | +|---|---:| +| Dialogue + scene + actor | Approximately 4.11 GiB | + +The request below is the operator's explicit authorization for those +downloads. Ensure the `model-cache` volume has enough additional capacity: + +```bash +curl --fail-with-body \ + --request POST \ + "https://${VIDXP_PUBLIC_API_HOST}/api/v1/jobs/model-preparation" \ + --header "Authorization: Bearer ${VIDXP_HTTP_STATIC_BEARER_TOKEN}" \ + --header "Idempotency-Key: initial-cpu-models-v1" \ + --header "Content-Type: application/json" \ + --data '{"modalities":["dialogue","scene","actor"],"capability_options":{}}' +``` + +The `202 Accepted` response contains the durable `job_id` and a `Location` +header. Poll that location until the job succeeds: + +```bash +curl --fail-with-body \ + --header "Authorization: Bearer ${VIDXP_HTTP_STATIC_BEARER_TOKEN}" \ + "https://${VIDXP_PUBLIC_API_HOST}/api/v1/jobs/" +``` + +The Streamable HTTP MCP `prepare_models` and `get_job` tools expose the same +operation for an authenticated agent client. Check +`/api/v1/runtime/readiness` afterward; `/ready` covers control-plane +availability and does not claim that every optional model is prepared. + +## Optional grounded query model + +Grounded retrieval works without a language model and returns timestamped +evidence. To enable generated claims, choose a model only after evaluating its +schema adherence, resource use, license, and grounding behavior. Set both: + +```dotenv +VIDXP_SLM_BASE_URL=http://ollama:11434/v1 +VIDXP_SLM_MODEL= +``` + +Then start the optional service and explicitly prepare the selected model: + +```bash +docker compose -f compose.coolify.yaml --profile slm up -d ollama +docker compose -f compose.coolify.yaml --profile slm exec ollama \ + ollama pull +docker compose -f compose.coolify.yaml up -d worker +``` + +The Compose deployment never pulls an SLM implicitly. Ollama is internal and +should not be published through the proxy. + +## Start and verify + +```bash +docker compose -f compose.coolify.yaml pull +docker compose -f compose.coolify.yaml up -d --wait +docker compose -f compose.coolify.yaml ps +``` + +The migration and readiness containers should exit successfully. PostgreSQL, API, +hooks, worker, and tusd should report healthy; Chroma is checked by the completed +`chroma-ready` gate. + +This is the supported server topology: one node, one API/MCP service, one hook +service, one CPU worker, and the bundled PostgreSQL, Chroma, tusd, and named +volumes. It is not a multi-replica, failover, or provider-portability design. Back +up PostgreSQL and the named data volumes before replacing a release. + +Treat each deployed stack as its singleton repository boundary. The current +PostgreSQL catalog and Chroma collections intentionally contain no repository +namespace. Deploy another complete stack, with separate databases and volumes, for +a separate repository. + +VidXP server mode connects to the internal Compose service names `postgres` and +`chroma`. Those endpoints are fixed by the supported topology: do not set +`VIDXP_DATABASE_URL` or `VIDXP_CHROMA_SERVER_URL`, and do not substitute hosted +PostgreSQL, hosted Chroma, or alternative database providers. diff --git a/docs/deployment/gpu-evaluation.md b/docs/deployment/gpu-evaluation.md new file mode 100644 index 0000000..c0e5f19 --- /dev/null +++ b/docs/deployment/gpu-evaluation.md @@ -0,0 +1,136 @@ +# GPU worker evaluation + +Status: evaluated on 2026-07-29; implementation and model execution are +intentionally deferred. + +No model, CUDA image, or Python wheel was pulled for this evaluation. Dependency +checks used registry metadata only. + +## Decision + +The first NVIDIA worker should use the supported CUDA 12.6 line: + +| Component | Evaluated version or policy | Decision | +| --- | --- | --- | +| Python | 3.14.6 | Keep the project-wide runtime | +| PyTorch | 2.13.0 `cu126` | Accept; latest stable Torch with its supported CUDA 12 fallback | +| CTranslate2 | 4.8.1 | Accept; current wheel supports CUDA 12 | +| faster-whisper | 1.2.1 | Accept with CUDA 12 cuBLAS and cuDNN 9 | +| cuDNN | 9.10.2.21 from the resolved `cu126` stack | Accept | +| Actor models | OpenCV CPU | Keep CPU-only; do not route through CUDA | +| Ollama | Separate optional service | Do not couple to the indexing worker GPU | + +CUDA 12.8 is rejected because the registry has no PyTorch 2.13 `cu128` +distribution. CUDA 12.9 resolved during metadata probing, but PyTorch 2.13's +published support direction retains CUDA 12.6 and moves the default to CUDA +13. CUDA 13 is rejected for the first worker because current CTranslate2 and +faster-whisper require the CUDA 12 family. Using 12.6 is a justified +compatibility exception, not an old default retained without review. + +The metadata-only resolver command was: + +```text +uv pip compile pyproject.toml + --extra server-worker + --no-sources + --python-version 3.14 + --python-platform x86_64-manylinux_2_28 + --torch-backend cu126 + --default-index https://pypi.org/simple + --index-strategy first-index +``` + +The exact implementation lock must be generated from `pyproject.toml` with the +`server-worker` extra and committed separately from the CPU `uv.lock`. +`--no-sources` is mandatory: without it, the current `tool.uv.sources` CPU +override wins and produces `torch+cpu` even when `--torch-backend cu126` is +present. `tool.uv.sources` must remain CPU-only for ordinary development and +publishing; the CUDA index is a release-resolver input, never a direct package +URL. + +## Capability routing + +| Capability operation | Device policy | Precision gate | +| --- | --- | --- | +| Scene SigLIP2 embedding/search | `cuda:` | Start with the current float32 contract; evaluate lower precision separately | +| Dialogue Qwen3 embedding/search | `cuda:` | Use bfloat16 only when `torch.cuda.is_bf16_supported()` passes; otherwise use an explicitly tested fallback | +| Dialogue transcription | CUDA device and index through faster-whisper | float16 | +| Actor detection/recognition and overlays | CPU | float32 | +| Media import and snippet extraction | CPU | Not applicable | +| SLM query planning/synthesis | External Ollama endpoint or deterministic fallback | Owned by the Ollama deployment | + +The existing durable queue split is directionally correct: CUDA repositories +submit model jobs to the GPU queue, while actor overlays, media import, and +snippets remain CPU jobs. An index job can still execute actor work on the GPU +worker process, but `ModelRuntime.device_for("actor")` keeps that operation on +CPU. + +## Required implementation boundary + +The GPU worker should remain a supplemental deployment, not alter the default +CPU/Coolify stack: + +1. Generate a target-specific GPU requirements lock using uv's `cu126` + resolver backend. +2. Add a dedicated worker image target based on the existing Python 3.14 + runtime. The resolved NVIDIA Python libraries supply CUDA 12.6/cuDNN 9; + configure their library directories for CTranslate2 before Python starts. +3. Add `compose.gpu.yaml` with one explicit `device_ids` entry and + `capabilities: [gpu]`. Do not expose all host GPUs implicitly. +4. Run only `vidxp-worker --role gpu` in that service and set + `VIDXP_RUNTIME_BACKEND=cuda:0` inside the container's visible-device + namespace. +5. Keep the CPU worker for CPU-queue jobs. Do not replace it with the GPU + worker. + +The host requires the NVIDIA Container Toolkit and a driver compatible with the +selected CUDA 12 runtime. Release qualification should use the CUDA 12.6 +toolkit driver floor rather than relying only on CUDA 12 minor-version +compatibility. + +## Readiness gaps to close + +Current worker health checks only verify PostgreSQL and Chroma. A GPU worker +must additionally fail readiness unless all of these are true: + +- the requested backend is exactly `cuda:`; +- `torch.backends.cuda.is_built()` and `torch.cuda.is_available()` are true; +- the selected index exists; +- the device capability appears in `torch.cuda.get_arch_list()`; +- the resolved PyTorch build reports CUDA 12.6; +- CTranslate2 reports at least one CUDA device; +- device name, capability, total memory, Torch version, CUDA version, cuDNN + version, and selected precision policy are exposed in structured readiness. + +`auto` must continue to select CPU. There must be no silent CPU fallback after +an explicit CUDA request. + +## Validation required before enablement + +No performance comparison is needed to enable the first functional GPU worker. +The release gate is correctness and failure behavior: + +- build the locked image without CPU Torch or CUDA 13 packages; +- start on a clean NVIDIA host and prove explicit device isolation; +- run dependency/readiness checks without model downloads; +- with already prepared model assets, smoke one scene index/search and one + dialogue transcription/search; +- verify actor processing remains on CPU; +- compare CPU and CUDA result shape, provenance, and retrieval tolerances; +- translate PyTorch OOM into a typed resource-limit failure; +- verify cancellation, worker restart, and DBOS recovery after OOM; +- record peak device memory and wall time without claiming an effectiveness + improvement. + +Blackwell, multi-GPU scheduling, MIG, ROCm, CUDA desktop installers, lower +precision tuning, and Ollama GPU sharing remain separate evaluations. + +## Current blockers + +Implementation must not begin until: + +- the target GPU/driver matrix is selected for CI; +- the separate CUDA lock format and update workflow are agreed; +- a CUDA host is available for readiness and no-model dependency validation; +- model assets are already cached for the two functional smokes; and +- CUDA/cuDNN redistribution notices and the generated image SBOM are reviewed. From 0ace642dd375c043fb6e2f7ea103f362b1394fa1 Mon Sep 17 00:00:00 2001 From: Talha Date: Fri, 31 Jul 2026 01:47:35 +0500 Subject: [PATCH 5/8] ci(release): adopt beta and stable channels --- .github/workflows/ci.yml | 270 +++++++++++++++++---- .github/workflows/containers.yml | 231 ++++++++++++++++++ .github/workflows/desktop.yml | 156 ++++++++++++ .github/workflows/promotion-pr.yml | 74 ++++++ .github/workflows/publish-desktop.yml | 129 ++++++++++ .github/workflows/release-please.yml | 67 +++++ .github/workflows/release-to-pypi.yml | 237 +++++++++--------- .github/workflows/release-to-test-pypi.yml | 156 ++++++++---- .github/workflows/security.yml | 16 ++ .github/workflows/sync-channels.yml | 115 +++++++++ .gitignore | 2 +- .release-please-manifest.json | 4 + .release-please-manifest.stable.json | 4 + CHANGELOG.md | 1 - changes/19.bugfix.md | 1 - changes/README.md | 30 --- desktop/CHANGELOG.md | 4 +- desktop/VERSION | 1 + desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/src/lib.rs | 10 +- docs/releasing.md | 103 +++++--- pyproject.toml | 60 ----- release-please-config.json | 85 +++++++ release-please-config.stable.json | 91 +++++++ tests/test_packaging.py | 152 ++++++++++++ utils/build-requirements.txt | 2 - utils/build_package.sh | 50 ++-- utils/verify_published_distribution.py | 77 ++++++ 29 files changed, 1751 insertions(+), 381 deletions(-) create mode 100644 .github/workflows/containers.yml create mode 100644 .github/workflows/desktop.yml create mode 100644 .github/workflows/promotion-pr.yml create mode 100644 .github/workflows/publish-desktop.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/sync-channels.yml create mode 100644 .release-please-manifest.json create mode 100644 .release-please-manifest.stable.json delete mode 100644 changes/19.bugfix.md delete mode 100644 changes/README.md create mode 100644 desktop/VERSION create mode 100644 release-please-config.json create mode 100644 release-please-config.stable.json create mode 100644 utils/verify_published_distribution.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13f799d..483331f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,65 @@ name: CI on: pull_request: + branches: + - main + - release types: - opened - synchronize - reopened + workflow_call: + inputs: + checkout_ref: + description: Exact commit to validate. + required: false + type: string + force_validation: + description: Run validation without PR path scoping. + required: false + default: false + type: boolean + python_versions: + description: JSON array of Python versions to validate. + required: false + default: '["3.14"]' + type: string + provider_platforms: + description: JSON array of runner labels for provider validation. + required: false + default: '["windows-2025"]' + type: string + run_containers: + description: Run container validation when validation is forced. + required: false + default: false + type: boolean + workflow_dispatch: + inputs: + checkout_ref: + description: Exact commit to validate. + required: true + type: string + force_validation: + description: Run validation without PR path scoping. + required: false + default: true + type: boolean + python_versions: + description: JSON array of Python versions to validate. + required: false + default: '["3.14"]' + type: string + provider_platforms: + description: JSON array of runner labels for provider validation. + required: false + default: '["windows-2025"]' + type: string + run_containers: + description: Run container validation. + required: false + default: true + type: boolean concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -16,90 +71,109 @@ permissions: pull-requests: read jobs: - validate: + scope: + if: >- + github.event_name != 'pull_request' || + github.base_ref != 'release' || + github.head_ref != 'main' runs-on: ubuntu-latest + outputs: + needs_python: ${{ steps.scope.outputs.needs_python }} + run_container: ${{ steps.scope.outputs.run_container }} + run_suite: ${{ steps.scope.outputs.run_suite }} steps: - uses: actions/checkout@v7 with: fetch-depth: 0 + ref: ${{ inputs.checkout_ref || github.sha }} - name: Determine validation scope id: scope shell: bash env: BASE_SHA: ${{ github.event.pull_request.base.sha }} - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} + FORCE_VALIDATION: ${{ inputs.force_validation || false }} + RUN_CONTAINERS: ${{ inputs.run_containers || false }} run: | + if [[ "$FORCE_VALIDATION" == "true" ]]; then + { + echo "run_suite=true" + echo "run_container=$RUN_CONTAINERS" + echo "needs_python=true" + } >> "$GITHUB_OUTPUT" + exit 0 + fi + changed_files="$(git diff --name-only "$BASE_SHA" HEAD)" - if grep -Eq '^(\.github/workflows/ci\.yml$|src/|tests/|utils/|LICENSE$|MANIFEST\.in$|pyproject\.toml$)' <<< "$changed_files"; then + if grep -Eq '^(\.github/workflows/ci\.yml$|src/|tests/|utils/|LICENSE$|MANIFEST\.in$|pyproject\.toml$|uv\.lock$)' <<< "$changed_files"; then run_suite=true else run_suite=false fi - if grep -Eq '^(\.dockerignore$|\.github/workflows/ci\.yml$|Dockerfile$|compose\.yaml$|pyproject\.toml$|src/vidxp/(requirements/.*\.txt|capabilities/[^/]+/requirements\.txt)$)' <<< "$changed_files"; then + if grep -Eq '^(\.dockerignore$|\.github/workflows/ci\.yml$|Dockerfile$|compose\.yaml$|pyproject\.toml$|uv\.lock$|src/vidxp/(requirements/.*\.txt|capabilities/[^/]+/requirements\.txt)$)' <<< "$changed_files"; then run_container=true else run_container=false fi - labels="$( - gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels" \ - --jq '.[].name' - )" - if grep -Eq '^(dependencies|skip-changelog)$' <<< "$labels"; then - require_changelog=false - else - require_changelog=true - fi - - if [[ "$run_suite" == "true" || "$require_changelog" == "true" ]]; then - needs_python=true - else - needs_python=false - fi - + needs_python="$run_suite" echo "run_suite=$run_suite" >> "$GITHUB_OUTPUT" echo "run_container=$run_container" >> "$GITHUB_OUTPUT" - echo "require_changelog=$require_changelog" >> "$GITHUB_OUTPUT" echo "needs_python=$needs_python" >> "$GITHUB_OUTPUT" + validate: + if: needs.scope.outputs.needs_python == 'true' + needs: scope + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ${{ fromJSON(inputs.python_versions || '["3.14"]') }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.checkout_ref || github.sha }} + - uses: actions/setup-python@v7 - if: steps.scope.outputs.needs_python == 'true' with: - python-version: "3.11" + python-version: ${{ matrix.python-version }} cache: pip - name: Install repository tooling - if: steps.scope.outputs.needs_python == 'true' run: | python -m pip install --upgrade pip python -m pip install -r utils/build-requirements.txt - - name: Require a changelog fragment - if: steps.scope.outputs.require_changelog == 'true' - run: towncrier check --compare-with "${{ github.event.pull_request.base.sha }}" - - name: Install test surface - if: steps.scope.outputs.run_suite == 'true' - run: python -m pip install ".[scene,frontend,benchmarks]" + if: needs.scope.outputs.run_suite == 'true' + run: | + python -m pip install "uv==0.12.0" + uv sync \ + --frozen \ + --extra all \ + --extra frontend \ + --extra benchmarks \ + --extra server \ + --extra slm \ + --extra test \ + --no-dev - name: Lint - if: steps.scope.outputs.run_suite == 'true' + if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14' run: ruff check src tests - name: Test - if: steps.scope.outputs.run_suite == 'true' - run: python -m unittest discover -s tests -q + if: needs.scope.outputs.run_suite == 'true' + run: uv run --no-sync python -m unittest discover -s tests -q env: PYTHONPATH: src - name: Build wheel and source distribution - if: steps.scope.outputs.run_suite == 'true' - run: python -m build + if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14' + run: bash utils/build_package.sh - name: Verify minimal wheel - if: steps.scope.outputs.run_suite == 'true' + if: needs.scope.outputs.run_suite == 'true' && matrix.python-version == '3.14' shell: bash run: | python -m venv .wheel-smoke @@ -111,38 +185,43 @@ jobs: from subprocess import check_output import vidxp - from vidxp.capabilities.registry import capability_names + from vidxp.capabilities.registry import create_capability_registry - assert capability_names() == ("dialogue", "scene", "actor") + assert create_capability_registry().names() == ( + "dialogue", "scene", "actor" + ) help_text = check_output( [".wheel-smoke/bin/vidxp", "--help"], text=True, ) - assert "benchmark" not in help_text + assert "benchmark" in help_text + benchmark_help = check_output( + [".wheel-smoke/bin/vidxp", "benchmark", "--help"], + text=True, + ) + assert "official benchmark adapters" in benchmark_help for module in ( "chromadb", - "clip", "cv2", - "face_recognition", + "faster_whisper", "sentence_transformers", - "srt", "streamlit", "torch", - "whisperx", + "transformers", ): assert find_spec(module) is None, module PY - name: Validate Compose configuration - if: steps.scope.outputs.run_container == 'true' + if: needs.scope.outputs.run_container == 'true' && matrix.python-version == '3.14' run: docker compose config --quiet - name: Build container - if: steps.scope.outputs.run_container == 'true' + if: needs.scope.outputs.run_container == 'true' && matrix.python-version == '3.14' run: docker build --tag vidxp:ci . - name: Smoke container - if: steps.scope.outputs.run_container == 'true' + if: needs.scope.outputs.run_container == 'true' && matrix.python-version == '3.14' shell: bash run: | docker run --detach --name vidxp-ci \ @@ -150,8 +229,11 @@ jobs: trap 'docker rm --force vidxp-ci >/dev/null 2>&1 || true' EXIT docker exec vidxp-ci vidxp --version + docker exec vidxp-ci vidxp init --json + docker exec vidxp-ci python -c \ + 'import chromadb, cv2, faster_whisper, huggingface_hub, pooch, psutil, sentence_transformers, torch, transformers; assert psutil.virtual_memory().available > 0' docker exec vidxp-ci python -c \ - 'import clip, face_recognition, whisperx' + 'import importlib.metadata as m; assert not any(d.metadata["Name"].lower().startswith("nvidia-") for d in m.distributions())' docker exec vidxp-ci sh -c \ 'test ! -d "$HOME/.cache/huggingface" && test ! -d "$HOME/.cache/clip"' @@ -167,3 +249,95 @@ jobs: docker logs vidxp-ci exit 1 + + provider-platform-smoke: + if: needs.scope.outputs.run_suite == 'true' + needs: scope + name: CPU providers (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ${{ fromJSON(inputs.provider_platforms || '["windows-2025"]') }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.checkout_ref || github.sha }} + + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - name: Install locked local-worker environment + run: | + python -m pip install "uv==0.12.0" + uv sync --frozen --extra local-worker --no-dev + + - name: Verify model provider contracts without network downloads + run: uv run --no-sync python -m unittest discover -s tests -p "test_models.py" -q + env: + PYTHONPATH: src + VIDXP_ALLOW_MODEL_DOWNLOADS: "false" + + - name: Verify indexing provider contracts without network downloads + run: uv run --no-sync python -m unittest discover -s tests -p "test_indexing.py" -q + env: + PYTHONPATH: src + VIDXP_ALLOW_MODEL_DOWNLOADS: "false" + + - name: Verify platform and CPU runtime + shell: bash + run: | + uv run --no-sync python - <<'PY' + from importlib import metadata + import platform + import chromadb + import cv2 + import faster_whisper + import huggingface_hub + import pooch + import psutil + import sentence_transformers + import torch + import transformers + + from vidxp.runtime import resolve_backends + + assert hasattr(cv2, "FaceDetectorYN") + assert hasattr(cv2, "FaceRecognizerSF") + assert hasattr(faster_whisper, "WhisperModel") + assert hasattr(faster_whisper, "BatchedInferencePipeline") + assert hasattr( + sentence_transformers.SentenceTransformer, + "encode_query", + ) + assert hasattr( + sentence_transformers.SentenceTransformer, + "encode_document", + ) + assert hasattr(transformers, "AutoModel") + assert hasattr(transformers, "AutoProcessor") + assert hasattr(chromadb, "PersistentClient") + assert hasattr(huggingface_hub, "snapshot_download") + assert hasattr(pooch, "retrieve") + assert psutil.virtual_memory().available > 0 + assert torch.version.cuda is None, torch.version.cuda + installed = { + distribution.metadata["Name"].lower().replace("_", "-") + for distribution in metadata.distributions() + if distribution.metadata.get("Name") + } + leaks = sorted( + name + for name in installed + if name in {"cuda-toolkit", "cuda-bindings", "triton"} + or name.startswith("nvidia-") + or name.startswith("pytorch-triton") + ) + assert not leaks, leaks + + profile = resolve_backends("cpu") + assert profile.torch_device == "cpu" + if platform.system() == "Darwin": + assert platform.machine() == "arm64" + PY diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml new file mode 100644 index 0000000..409d2ea --- /dev/null +++ b/.github/workflows/containers.yml @@ -0,0 +1,231 @@ +name: Publish core containers + +on: + workflow_call: + inputs: + channel: + description: Container channel to publish. + required: true + type: string + tag: + description: Existing core tag to build. + required: true + type: string + version: + description: Core version without the v prefix. + required: true + type: string + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag }} + + - name: Resolve source revision + id: source + shell: bash + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@v4 + + - name: Generate product image metadata + id: metadata + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=${{ inputs.version }} + type=semver,pattern={{major}}.{{minor}},value=${{ inputs.tag }},enable=${{ inputs.channel == 'stable' }} + type=raw,value=latest,enable=${{ inputs.channel == 'stable' }} + type=raw,value=beta,enable=${{ inputs.channel == 'beta' }} + labels: | + org.opencontainers.image.title=VidXP + org.opencontainers.image.description=Local-first video indexing and search + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ steps.source.outputs.sha }} + org.opencontainers.image.version=${{ inputs.version }} + org.opencontainers.image.licenses=MIT + + - name: Build product image + uses: docker/build-push-action@v7 + with: + context: . + load: true + platforms: linux/amd64 + push: false + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha,scope=release-product + cache-to: type=gha,mode=max,scope=release-product + + - name: Build control image + uses: docker/build-push-action@v7 + with: + context: . + load: true + platforms: linux/amd64 + push: false + target: control + tags: | + ghcr.io/${{ github.repository }}:${{ inputs.version }}-control + ghcr.io/${{ github.repository }}:${{ inputs.channel == 'stable' && 'latest' || 'beta' }}-control + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha,scope=release-control + cache-to: type=gha,mode=max,scope=release-control + + - name: Build worker image + uses: docker/build-push-action@v7 + with: + context: . + load: true + platforms: linux/amd64 + push: false + target: worker + tags: | + ghcr.io/${{ github.repository }}:${{ inputs.version }}-worker + ghcr.io/${{ github.repository }}:${{ inputs.channel == 'stable' && 'latest' || 'beta' }}-worker + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha,scope=release-worker + cache-to: type=gha,mode=max,scope=release-worker + + - name: Smoke product image + shell: bash + run: | + image="ghcr.io/${{ github.repository }}:${{ inputs.version }}" + docker run --detach --name vidxp-release \ + --publish 127.0.0.1:8501:8501 "$image" + trap 'docker rm --force vidxp-release >/dev/null 2>&1 || true' EXIT + + docker exec vidxp-release vidxp --version + docker exec vidxp-release vidxp init --json + docker exec vidxp-release python -c \ + 'import chromadb, cv2, faster_whisper, huggingface_hub, pooch, psutil, sentence_transformers, torch, transformers; assert psutil.virtual_memory().available > 0' + docker exec vidxp-release python -c \ + 'import importlib.metadata as m; assert not any(d.metadata["Name"].lower().startswith("nvidia-") for d in m.distributions())' + docker exec vidxp-release sh -c \ + 'test ! -d "$HOME/.cache/huggingface" && test ! -d "$HOME/.cache/clip"' + + for attempt in {1..30}; do + if curl --fail --silent http://127.0.0.1:8501/_stcore/health; then + exit 0 + fi + if [[ "$(docker inspect --format='{{.State.Running}}' vidxp-release)" != "true" ]]; then + break + fi + sleep 2 + done + + docker logs vidxp-release + exit 1 + + - name: Smoke server images + shell: bash + run: | + control="ghcr.io/${{ github.repository }}:${{ inputs.version }}-control" + worker="ghcr.io/${{ github.repository }}:${{ inputs.version }}-worker" + docker run --rm "$control" vidxp-api --help + docker run --rm "$control" vidxp-mcp --help + docker run --rm "$worker" vidxp init --json + docker run --rm "$worker" python -c \ + 'import chromadb, cv2, faster_whisper, huggingface_hub, pooch, psutil, sentence_transformers, torch, transformers; assert psutil.virtual_memory().available > 0' + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Protect immutable version tags + id: existing + env: + SOURCE_SHA: ${{ steps.source.outputs.sha }} + shell: bash + run: | + check_version_tag() { + local name="$1" + local image="$2" + local local_image="vidxp-release-local:$name" + + docker tag "$image" "$local_image" + if ! docker pull "$image" >/dev/null 2>&1; then + docker tag "$local_image" "$image" + echo "${name}=false" >> "$GITHUB_OUTPUT" + return + fi + + revision="$( + docker inspect \ + --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' \ + "$image" + )" + if [[ "$revision" != "$SOURCE_SHA" ]]; then + echo "::error::$image already belongs to source revision $revision, not $SOURCE_SHA." + exit 1 + fi + echo "${name}=true" >> "$GITHUB_OUTPUT" + } + + check_version_tag \ + product \ + "ghcr.io/${{ github.repository }}:${{ inputs.version }}" + check_version_tag \ + control \ + "ghcr.io/${{ github.repository }}:${{ inputs.version }}-control" + check_version_tag \ + worker \ + "ghcr.io/${{ github.repository }}:${{ inputs.version }}-worker" + + - name: Publish containers + shell: bash + run: | + product="ghcr.io/${{ github.repository }}:${{ inputs.version }}" + control="${product}-control" + worker="${product}-worker" + control_channel="ghcr.io/${{ github.repository }}:${{ inputs.channel == 'stable' && 'latest' || 'beta' }}-control" + worker_channel="ghcr.io/${{ github.repository }}:${{ inputs.channel == 'stable' && 'latest' || 'beta' }}-worker" + + if [[ "${{ steps.existing.outputs.product }}" == "true" ]]; then + while read -r image; do + [[ "$image" == "$product" ]] || docker tag "$product" "$image" + done <<< "${{ steps.metadata.outputs.tags }}" + fi + while read -r image; do + if [[ "$image" != "$product" || + "${{ steps.existing.outputs.product }}" != "true" ]]; then + docker push "$image" + fi + done <<< "${{ steps.metadata.outputs.tags }}" + + if [[ "${{ steps.existing.outputs.control }}" == "true" ]]; then + docker tag "$control" "$control_channel" + else + docker push "$control" + fi + docker push "$control_channel" + + if [[ "${{ steps.existing.outputs.worker }}" == "true" ]]; then + docker tag "$worker" "$worker_channel" + else + docker push "$worker" + fi + docker push "$worker_channel" + + - name: Verify public container access + shell: bash + run: | + docker logout ghcr.io + for suffix in "" "-control" "-worker"; do + image="ghcr.io/${{ github.repository }}:${{ inputs.version }}${suffix}" + if ! docker buildx imagetools inspect "$image"; then + echo "::error::Make the GHCR package public, then rerun this workflow for the same tag." + exit 1 + fi + done diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 0000000..e2c27d0 --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,156 @@ +name: Desktop + +on: + workflow_call: + inputs: + checkout_ref: + description: Commit to build + required: true + type: string + workflow_dispatch: + inputs: + checkout_ref: + description: Commit, branch, or tag to build + required: false + default: "" + type: string + pull_request: + branches: + - main + - release + paths: + - "desktop/**" + - "src/vidxp/requirements/**" + - "src/vidxp/capabilities/*/requirements.txt" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/desktop.yml" + +permissions: + contents: read + +concurrency: + group: desktop-${{ github.event.pull_request.number || inputs.checkout_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + if: >- + github.event_name != 'pull_request' || + github.base_ref != 'release' || + github.head_ref != 'main' + name: ${{ matrix.target == 'windows' && 'Windows x86-64 NSIS' || matrix.target == 'macos' && 'macOS Apple Silicon DMG' || 'Linux x86-64 AppImage' }} + runs-on: ${{ matrix.target == 'windows' && 'windows-2025' || matrix.target == 'macos' && 'macos-15' || 'ubuntu-24.04' }} + strategy: + fail-fast: false + matrix: + target: ${{ fromJSON(github.event_name == 'pull_request' && !startsWith(github.head_ref, 'release-please--branches--') && '["windows"]' || '["windows","macos","linux"]') }} + + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.checkout_ref || github.event.inputs.checkout_ref || github.sha }} + + - name: Install Linux desktop build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + file \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libxdo-dev + + - uses: actions/setup-node@v7 + with: + node-version: "22" + cache: npm + cache-dependency-path: desktop/package-lock.json + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.12.0" + enable-cache: false + + - uses: dtolnay/rust-toolchain@1.97.1 + + - name: Restore Rust build cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + desktop/src-tauri/target + key: desktop-rust-${{ runner.os }}-${{ runner.arch }}-1.97.1-${{ hashFiles('desktop/src-tauri/Cargo.lock') }} + restore-keys: | + desktop-rust-${{ runner.os }}-${{ runner.arch }}-1.97.1- + + - name: Install locked desktop tooling + run: npm ci + working-directory: desktop + + - name: Verify the desktop runtime constraints + shell: bash + run: | + generated="$RUNNER_TEMP/runtime-constraints.txt" + uv export \ + --frozen \ + --extra local-worker \ + --extra frontend \ + --no-dev \ + --no-emit-project \ + --no-hashes \ + --format requirements-txt \ + --output-file "$generated" + git diff --no-index --ignore-matching-lines='^#' -- \ + desktop/runtime-constraints.txt "$generated" + + - name: Restore the pinned uv sidecar + id: uv-sidecar-cache + uses: actions/cache@v5 + with: + path: desktop/src-tauri/binaries + key: desktop-uv-sidecar-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('desktop/sidecars.json') }} + + - name: Fetch the pinned uv sidecar + if: steps.uv-sidecar-cache.outputs.cache-hit != 'true' + run: npm run ${{ matrix.target == 'windows' && 'sidecar:windows' || 'sidecar:unix' }} + working-directory: desktop + + - name: Test the locked desktop crate + run: cargo test --release --locked --manifest-path desktop/src-tauri/Cargo.toml + + - name: Build the unsigned desktop installer + run: npm run desktop:build + working-directory: desktop + + - name: Verify the Windows app uses the GUI subsystem + if: runner.os == 'Windows' + shell: pwsh + run: | + $path = "desktop/src-tauri/target/release/vidxp-desktop.exe" + $stream = [System.IO.File]::OpenRead($path) + try { + $reader = [System.IO.BinaryReader]::new($stream) + $stream.Position = 0x3c + $peOffset = $reader.ReadInt32() + $stream.Position = $peOffset + 4 + 20 + 68 + $subsystem = $reader.ReadUInt16() + if ($subsystem -ne 2) { + throw "Expected Windows GUI subsystem 2, found $subsystem." + } + } finally { + $stream.Dispose() + } + + - name: Preserve the desktop installer + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.target == 'windows' && 'vidxp-desktop-windows-x86_64' || matrix.target == 'macos' && 'vidxp-desktop-macos-aarch64' || 'vidxp-desktop-linux-x86_64' }} + path: ${{ matrix.target == 'windows' && 'desktop/src-tauri/target/release/bundle/nsis/*-setup.exe' || matrix.target == 'macos' && 'desktop/src-tauri/target/release/bundle/dmg/*.dmg' || 'desktop/src-tauri/target/release/bundle/appimage/*.AppImage' }} + if-no-files-found: error + compression-level: 0 + retention-days: 14 diff --git a/.github/workflows/promotion-pr.yml b/.github/workflows/promotion-pr.yml new file mode 100644 index 0000000..8c3103f --- /dev/null +++ b/.github/workflows/promotion-pr.yml @@ -0,0 +1,74 @@ +name: Maintain stable promotion PR + +on: + push: + branches: + - main + pull_request_target: + branches: + - release + types: + - closed + schedule: + - cron: "17 * * * *" + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: stable-promotion-pr + cancel-in-progress: false + +jobs: + ensure: + if: >- + github.event_name != 'pull_request_target' || + ( + github.event.pull_request.head.ref == 'main' && + github.event.pull_request.head.repo.full_name == github.repository + ) + runs-on: ubuntu-latest + steps: + - name: Ensure a current main-to-release promotion PR exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} + shell: bash + run: | + comparison="$( + gh api "repos/$REPOSITORY/compare/release...main" \ + --jq '[.status, .ahead_by] | @tsv' + )" + IFS=$'\t' read -r status ahead <<< "$comparison" + if [[ "$status" == "diverged" ]]; then + echo "release and main diverged; stable synchronization must finish first." + exit 0 + fi + if [[ "$ahead" == "0" ]]; then + echo "main has no commits to promote." + exit 0 + fi + + existing="$( + gh pr list \ + --repo "$REPOSITORY" \ + --base release \ + --head main \ + --state open \ + --json number \ + --jq '.[0].number // empty' + )" + if [[ -n "$existing" ]]; then + echo "Promotion PR #$existing already tracks main." + exit 0 + fi + + gh pr create \ + --repo "$REPOSITORY" \ + --base release \ + --head main \ + --draft \ + --title "chore(release): promote beta to stable" \ + --body "This draft is maintained automatically. Mark it ready and merge it when the current beta is approved for stable release." diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml new file mode 100644 index 0000000..1ef287a --- /dev/null +++ b/.github/workflows/publish-desktop.yml @@ -0,0 +1,129 @@ +name: Publish desktop release + +on: + workflow_dispatch: + inputs: + tag: + description: Existing desktop tag to build and publish. + required: true + type: string + +permissions: + contents: read + +concurrency: + group: desktop-release-${{ inputs.tag }} + cancel-in-progress: false + +jobs: + verify: + runs-on: ubuntu-latest + outputs: + source_sha: ${{ steps.source.outputs.sha }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag }} + + - name: Resolve source revision + id: source + shell: bash + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Verify desktop tag and version sources + shell: bash + run: | + tag="${{ inputs.tag }}" + if [[ "$tag" != desktop-v* ]]; then + echo "::error::$tag is not a desktop release tag." + exit 1 + fi + if ! git show-ref --verify --quiet "refs/tags/$tag"; then + echo "::error::$tag is not an existing Git tag." + exit 1 + fi + python - "${tag#desktop-v}" <<'PY' + import json + from pathlib import Path + import sys + import tomllib + + expected = sys.argv[1] + desktop = Path("desktop") + runtime_manifest = json.loads( + (desktop / "runtime-manifest.json").read_text() + ) + versions = { + "VERSION": (desktop / "VERSION").read_text().strip(), + "Cargo.toml": tomllib.loads( + (desktop / "src-tauri" / "Cargo.toml").read_text() + )["package"]["version"], + "package.json": json.loads( + (desktop / "package.json").read_text() + )["version"], + "package-lock.json": json.loads( + (desktop / "package-lock.json").read_text() + )["version"], + "package-lock root": json.loads( + (desktop / "package-lock.json").read_text() + )["packages"][""]["version"], + "runtime manifest desktop": runtime_manifest["desktop_version"], + "runtime manifest package": runtime_manifest["package_version"], + "Tauri config": json.loads( + (desktop / "src-tauri" / "tauri.conf.json").read_text() + )["version"], + } + mismatches = { + source: actual + for source, actual in versions.items() + if actual != expected + } + if mismatches: + raise SystemExit( + f"desktop tag expects {expected}; mismatches: {mismatches}" + ) + PY + + build: + needs: verify + uses: ./.github/workflows/desktop.yml + with: + checkout_ref: ${{ inputs.tag }} + + publish: + needs: + - build + - verify + runs-on: ubuntu-latest + permissions: + actions: write + contents: write + steps: + - uses: actions/download-artifact@v8 + with: + pattern: vidxp-desktop-* + path: desktop-dist/ + merge-multiple: true + + - name: Publish complete desktop release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + tag="${{ inputs.tag }}" + version="${tag#desktop-v}" + gh release upload "$tag" desktop-dist/* --clobber + if [[ "$version" == *-* ]]; then + gh release edit "$tag" --draft=false --prerelease + else + gh release edit "$tag" --draft=false --latest + fi + + - name: Synchronize stable source back to main + if: ${{ !contains(inputs.tag, '-b.') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh workflow run sync-channels.yml \ + --ref main \ + -f release_sha="${{ needs.verify.outputs.source_sha }}" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..04e3051 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,67 @@ +name: Prepare releases + +on: + push: + branches: + - main + - release + workflow_dispatch: + +permissions: + actions: write + contents: write + issues: write + pull-requests: write + +concurrency: + group: release-please-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Enable the beta publication workflow + if: github.ref_name == 'main' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + gh api --method PUT + "repos/$GITHUB_REPOSITORY/actions/workflows/release-to-test-pypi.yml/enable" + + - name: Prepare or create component releases + id: release + uses: googleapis/release-please-action@v5 + with: + config-file: ${{ github.ref_name == 'release' && 'release-please-config.stable.json' || 'release-please-config.json' }} + manifest-file: ${{ github.ref_name == 'release' && '.release-please-manifest.stable.json' || '.release-please-manifest.json' }} + target-branch: ${{ github.ref_name }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish the core release + if: steps.release.outputs.release_created == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.release.outputs.tag_name }} + shell: bash + run: | + if [[ "${{ github.ref_name }}" == "release" ]]; then + workflow="release-to-pypi.yml" + else + workflow="release-to-test-pypi.yml" + fi + gh workflow run "$workflow" \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$TAG" \ + -f tag="$TAG" + + - name: Publish the desktop release + if: steps.release.outputs['desktop--release_created'] == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.release.outputs['desktop--tag_name'] }} + run: | + gh workflow run publish-desktop.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$TAG" \ + -f tag="$TAG" diff --git a/.github/workflows/release-to-pypi.yml b/.github/workflows/release-to-pypi.yml index 8eef5de..5a3688f 100644 --- a/.github/workflows/release-to-pypi.yml +++ b/.github/workflows/release-to-pypi.yml @@ -1,51 +1,83 @@ -name: Release (main → PyPI) +name: Release (tag → PyPI and GHCR) on: workflow_dispatch: + inputs: + tag: + description: Existing stable core tag to publish. + required: true + type: string permissions: contents: read + pull-requests: read concurrency: - group: pypi-main + group: pypi-${{ inputs.tag }} cancel-in-progress: false jobs: - release: - if: github.ref == 'refs/heads/main' + validate: + uses: ./.github/workflows/ci.yml + with: + checkout_ref: ${{ inputs.tag }} + force_validation: true + python_versions: '["3.11","3.12","3.13","3.14"]' + provider_platforms: '["ubuntu-24.04","windows-2025","macos-15"]' + run_containers: false + + build: + needs: validate runs-on: ubuntu-latest - permissions: - contents: write outputs: - commit_sha: ${{ steps.release.outputs.commit_sha }} - released: ${{ steps.release.outputs.released }} - tag: ${{ steps.release.outputs.tag }} - version: ${{ steps.release.outputs.version }} - + source_sha: ${{ steps.source.outputs.sha }} + version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v7 with: - fetch-depth: 0 + ref: ${{ inputs.tag }} - - name: Set up Python - uses: actions/setup-python@v7 + - name: Resolve source revision + id: source + shell: bash + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-python@v7 with: - python-version: "3.11" + python-version: "3.14" + cache: pip - - name: Install dependencies + - name: Verify stable tag + id: version + shell: bash run: | - python -m pip install --upgrade pip - python -m pip install -r utils/build-requirements.txt + tag="${{ inputs.tag }}" + if [[ "$tag" != v* || "$tag" == *-* ]]; then + echo "::error::$tag is not a stable core release tag." + exit 1 + fi + if ! git show-ref --verify --quiet "refs/tags/$tag"; then + echo "::error::$tag is not an existing Git tag." + exit 1 + fi + version="${tag#v}" + package_version="$( + python -c \ + 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])' + )" + if [[ "$package_version" != "$version" ]]; then + echo "::error::Tag $tag does not match package version $package_version." + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Create stable release - id: release - env: - BUILD_CHANGELOG: "1" - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: semantic-release version --no-changelog + - name: Install build tooling + run: python -m pip install -r utils/build-requirements.txt + + - name: Build distribution + run: bash utils/build_package.sh - name: Smoke distribution - if: steps.release.outputs.released == 'true' shell: bash run: | python -m venv .release-smoke @@ -53,130 +85,89 @@ jobs: .release-smoke/bin/python -m pip check .release-smoke/bin/vidxp --version - - name: Publish GitHub release - if: steps.release.outputs.released == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - semantic-release publish --tag "${{ steps.release.outputs.tag }}" - gh release edit "${{ steps.release.outputs.tag }}" \ - --notes-file .release-notes.md - - - name: Preserve distribution - if: steps.release.outputs.released == 'true' - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7 with: - name: pypi-distribution + name: pypi-${{ inputs.tag }} path: dist/ if-no-files-found: error - retention-days: 7 + retention-days: 14 publish-pypi: - if: needs.release.outputs.released == 'true' - needs: release + needs: build runs-on: ubuntu-latest environment: pypi permissions: + contents: read id-token: write - steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag }} + - uses: actions/download-artifact@v8 with: - name: pypi-distribution + name: pypi-${{ inputs.tag }} path: dist/ + - name: Check PyPI release state + id: registry + shell: bash + run: | + state="$( + python utils/verify_published_distribution.py \ + --repository https://pypi.org \ + --package vidxp \ + --version "${{ needs.build.outputs.version }}" \ + --dist dist + )" + echo "state=$state" >> "$GITHUB_OUTPUT" + - name: Publish to PyPI + if: steps.registry.outputs.state == 'absent' uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ - publish-container: - if: >- - github.ref == 'refs/heads/main' && - needs.release.outputs.released == 'true' - needs: release - runs-on: ubuntu-latest + publish-containers: + needs: + - build + - publish-pypi permissions: contents: read packages: write - + uses: ./.github/workflows/containers.yml + with: + channel: stable + tag: ${{ inputs.tag }} + version: ${{ needs.build.outputs.version }} + + publish-github: + needs: + - build + - publish-pypi + - publish-containers + runs-on: ubuntu-latest + permissions: + actions: write + contents: write steps: - - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 with: - ref: ${{ needs.release.outputs.commit_sha }} - - - uses: docker/setup-buildx-action@v4 + name: pypi-${{ inputs.tag }} + path: dist/ - - name: Generate container metadata - id: metadata - uses: docker/metadata-action@v6 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern={{version}},value=${{ needs.release.outputs.tag }} - type=semver,pattern={{major}}.{{minor}},value=${{ needs.release.outputs.tag }} - type=raw,value=latest - labels: | - org.opencontainers.image.title=VidXP - org.opencontainers.image.description=Local-first video indexing and search - org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ needs.release.outputs.commit_sha }} - org.opencontainers.image.version=${{ needs.release.outputs.version }} - org.opencontainers.image.licenses=MIT - - - name: Build stable container - uses: docker/build-push-action@v7 - with: - context: . - load: true - platforms: linux/amd64 - push: false - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - - - name: Smoke stable container + - name: Publish complete stable release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | - image="ghcr.io/${{ github.repository }}:${{ needs.release.outputs.version }}" - docker run --detach --name vidxp-release \ - --publish 127.0.0.1:8501:8501 "$image" - trap 'docker rm --force vidxp-release >/dev/null 2>&1 || true' EXIT - - docker exec vidxp-release vidxp --version - docker exec vidxp-release python -c \ - 'import clip, face_recognition, whisperx' - docker exec vidxp-release sh -c \ - 'test ! -d "$HOME/.cache/huggingface" && test ! -d "$HOME/.cache/clip"' - - for attempt in {1..30}; do - if curl --fail --silent http://127.0.0.1:8501/_stcore/health; then - exit 0 - fi - if [[ "$(docker inspect --format='{{.State.Running}}' vidxp-release)" != "true" ]]; then - break - fi - sleep 2 - done - - docker logs vidxp-release - exit 1 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish stable container - run: docker push --all-tags "ghcr.io/${{ github.repository }}" + gh release upload "${{ inputs.tag }}" dist/* --clobber + gh release edit "${{ inputs.tag }}" --draft=false - - name: Verify public container access - shell: bash + - name: Synchronize stable source back to main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - docker logout ghcr.io - image="ghcr.io/${{ github.repository }}:${{ needs.release.outputs.version }}" - if ! docker buildx imagetools inspect "$image"; then - echo "::error::Make the GHCR package public, then rerun this failed job." - exit 1 - fi + gh workflow run sync-channels.yml \ + --ref main \ + -f release_sha="${{ needs.build.outputs.source_sha }}" diff --git a/.github/workflows/release-to-test-pypi.yml b/.github/workflows/release-to-test-pypi.yml index f9c55e6..de2f463 100644 --- a/.github/workflows/release-to-test-pypi.yml +++ b/.github/workflows/release-to-test-pypi.yml @@ -1,59 +1,66 @@ -name: Prerelease (main → TestPyPI) +name: Prerelease (tag → TestPyPI) on: - push: - branches: - - main - paths: - - "src/**" - - "pyproject.toml" - - "README.md" - - "CHANGELOG.md" - - "changes/**" - - "LICENSE" - - "MANIFEST.in" - - "utils/**" - - ".github/workflows/release-to-test-pypi.yml" + workflow_dispatch: + inputs: + tag: + description: Existing core prerelease tag to publish. + required: true + type: string permissions: contents: read concurrency: - group: testpypi-${{ github.ref }} + group: testpypi-${{ inputs.tag }} cancel-in-progress: false jobs: - prerelease: + build: runs-on: ubuntu-latest - permissions: - contents: write outputs: - released: ${{ steps.release.outputs.released }} - + version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v7 with: - fetch-depth: 0 + ref: ${{ inputs.tag }} - - name: Set up Python - uses: actions/setup-python@v7 + - uses: actions/setup-python@v7 with: - python-version: "3.11" + python-version: "3.14" + cache: pip - - name: Install dependencies + - name: Verify prerelease tag + id: version + shell: bash run: | - python -m pip install --upgrade pip - python -m pip install -r utils/build-requirements.txt + tag="${{ inputs.tag }}" + if [[ "$tag" != v*-* ]]; then + echo "::error::$tag is not a core prerelease tag." + exit 1 + fi + if ! git show-ref --verify --quiet "refs/tags/$tag"; then + echo "::error::$tag is not an existing Git tag." + exit 1 + fi + version="${tag#v}" + package_version="$( + python -c \ + 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])' + )" + if [[ "$package_version" != "$version" ]]; then + echo "::error::Tag $tag does not match package version $package_version." + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Determine and tag prerelease version - id: release - env: - BUILD_RELEASE_NOTES: "1" - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: semantic-release version --as-prerelease --prerelease-token b --no-changelog + - name: Install build tooling + run: python -m pip install -r utils/build-requirements.txt + + - name: Build distribution + run: bash utils/build_package.sh - name: Smoke distribution - if: steps.release.outputs.released == 'true' shell: bash run: | python -m venv .release-smoke @@ -61,40 +68,83 @@ jobs: .release-smoke/bin/python -m pip check .release-smoke/bin/vidxp --version - - name: Publish GitHub release - if: steps.release.outputs.released == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - semantic-release publish --tag "${{ steps.release.outputs.tag }}" - gh release edit "${{ steps.release.outputs.tag }}" \ - --notes-file .release-notes.md - - - name: Preserve distribution - if: steps.release.outputs.released == 'true' - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7 with: - name: testpypi-distribution + name: testpypi-${{ inputs.tag }} path: dist/ if-no-files-found: error - retention-days: 7 + retention-days: 14 - publish-testpypi: - if: needs.prerelease.outputs.released == 'true' - needs: prerelease + publish: + needs: build runs-on: ubuntu-latest environment: testpypi permissions: + contents: read id-token: write - steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag }} + - uses: actions/download-artifact@v8 with: - name: testpypi-distribution + name: testpypi-${{ inputs.tag }} path: dist/ + - name: Check TestPyPI release state + id: registry + shell: bash + run: | + state="$( + python utils/verify_published_distribution.py \ + --repository https://test.pypi.org \ + --package vidxp \ + --version "${{ needs.build.outputs.version }}" \ + --dist dist + )" + echo "state=$state" >> "$GITHUB_OUTPUT" + - name: Publish to TestPyPI + if: steps.registry.outputs.state == 'absent' uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ packages-dir: dist/ + + publish-containers: + needs: + - build + - publish + permissions: + contents: read + packages: write + uses: ./.github/workflows/containers.yml + with: + channel: beta + tag: ${{ inputs.tag }} + version: ${{ needs.build.outputs.version }} + + publish-github: + needs: + - build + - publish + - publish-containers + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v8 + with: + name: testpypi-${{ inputs.tag }} + path: dist/ + + - name: Publish complete prerelease + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release upload "${{ inputs.tag }}" dist/* --clobber + gh release edit "${{ inputs.tag }}" \ + --draft=false \ + --prerelease diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 268772c..b3a2765 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -2,12 +2,22 @@ name: Security on: pull_request: + branches: + - main + - release paths: - ".github/dependabot.yml" - ".github/workflows/**" - "pyproject.toml" + - "uv.lock" - "src/**/*.txt" - "utils/build-requirements.txt" + - "desktop/package.json" + - "desktop/package-lock.json" + - "desktop/runtime-constraints.txt" + - "desktop/sidecars.json" + - "desktop/src-tauri/Cargo.toml" + - "desktop/src-tauri/Cargo.lock" concurrency: group: security-${{ github.event.pull_request.number || github.ref }} @@ -18,9 +28,15 @@ permissions: jobs: dependency-review: + if: >- + github.base_ref != 'release' || + github.head_ref != 'main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/dependency-review-action@v5 with: fail-on-severity: high + # No patched Chroma release exists yet. Chroma is kept on the + # deployment's private network and is never published directly. + allow-ghsas: GHSA-f4j7-r4q5-qw2c diff --git a/.github/workflows/sync-channels.yml b/.github/workflows/sync-channels.yml new file mode 100644 index 0000000..1578f20 --- /dev/null +++ b/.github/workflows/sync-channels.yml @@ -0,0 +1,115 @@ +name: Synchronize release channels + +on: + schedule: + - cron: "41 * * * *" + workflow_dispatch: + inputs: + release_sha: + description: Stable release commit that completed publication. + required: false + default: "" + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: synchronize-release-channels + cancel-in-progress: false + +jobs: + synchronize: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: release + + - name: Fast-forward main after complete stable publication + env: + EXPECTED_RELEASE_SHA: ${{ inputs.release_sha }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} + shell: bash + run: | + git fetch --force origin \ + refs/heads/main:refs/remotes/origin/main \ + refs/heads/release:refs/remotes/origin/release \ + '+refs/tags/*:refs/tags/*' + + release_sha="$(git rev-parse origin/release)" + main_sha="$(git rev-parse origin/main)" + if [[ -n "$EXPECTED_RELEASE_SHA" ]] && + ! git merge-base --is-ancestor "$EXPECTED_RELEASE_SHA" "$release_sha"; then + echo "::error::Published release $EXPECTED_RELEASE_SHA is not in the release branch." + exit 1 + fi + publication_sha="${EXPECTED_RELEASE_SHA:-$release_sha}" + + matched=0 + drafts=0 + while IFS=$'\t' read -r tag draft prerelease; do + [[ -n "$tag" ]] || continue + if ! git rev-parse --verify --quiet "refs/tags/$tag^{commit}" >/dev/null; then + continue + fi + tag_sha="$(git rev-parse "refs/tags/$tag^{commit}")" + if [[ "$tag_sha" != "$publication_sha" || "$prerelease" == "true" ]]; then + continue + fi + matched=$((matched + 1)) + if [[ "$draft" == "true" ]]; then + drafts=$((drafts + 1)) + fi + done < <( + gh api --paginate "repos/$REPOSITORY/releases?per_page=100" \ + --jq '.[] | [.tag_name, .draft, .prerelease] | @tsv' + ) + + if [[ "$matched" == "0" ]]; then + echo "release has not produced a stable GitHub release yet." + exit 0 + fi + if [[ "$drafts" != "0" ]]; then + echo "$drafts stable release artifact set(s) are still draft." + exit 0 + fi + + if [[ "$main_sha" == "$publication_sha" ]] || + git merge-base --is-ancestor "$publication_sha" "$main_sha"; then + echo "main already contains published release $publication_sha." + exit 0 + fi + if git merge-base --is-ancestor "$main_sha" "$publication_sha"; then + git push origin "$publication_sha:refs/heads/main" + exit 0 + fi + + if [[ "$publication_sha" != "$release_sha" ]]; then + echo "::error::release advanced beyond published commit $publication_sha; refusing to propose unrelated changes." + exit 1 + fi + + existing="$( + gh pr list \ + --repo "$REPOSITORY" \ + --base main \ + --head release \ + --state open \ + --json number \ + --jq '.[0].number // empty' + )" + if [[ -n "$existing" ]]; then + echo "Stable synchronization PR #$existing already exists." + exit 0 + fi + + gh pr create \ + --repo "$REPOSITORY" \ + --base main \ + --head release \ + --title "chore(release): synchronize stable baseline" \ + --body "Stable publication completed, but main advanced during the release. Merge this PR to carry the published version baseline back without discarding newer main commits." diff --git a/.gitignore b/.gitignore index 55a6fdd..49668d6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /video.mp4 /dist/ /build/ -/.release-notes.md +/release-notes.md .build /MANIFEST /.pytest_cache/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..c3185f6 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,4 @@ +{ + ".": "0.2.1-b.1", + "desktop": "0.2.1-b.1" +} diff --git a/.release-please-manifest.stable.json b/.release-please-manifest.stable.json new file mode 100644 index 0000000..25db77c --- /dev/null +++ b/.release-please-manifest.stable.json @@ -0,0 +1,4 @@ +{ + ".": "0.2.0", + "desktop": "0.2.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1ff47..697f552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,5 @@ # CHANGELOG - ## v0.2.0 (2026-07-28) ### Features diff --git a/changes/19.bugfix.md b/changes/19.bugfix.md deleted file mode 100644 index 2c79f5a..0000000 --- a/changes/19.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Prevent dependency updates and label changes from spawning redundant CI runs. diff --git a/changes/README.md b/changes/README.md deleted file mode 100644 index c264b27..0000000 --- a/changes/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Changelog fragments - -Every pull request with a user-visible change must add one short fragment here. -Use the pull request number and the most specific type: - -```text -changes/..md -``` - -Supported types are `breaking`, `feature`, `bugfix`, `deprecation`, `docs`, and -`security`. Write one sentence for users in the imperative voice and do not add -a heading or the version number. - -Example: - -```text -changes/123.feature.md -``` - -```markdown -Add named repositories for selecting shared index locations and devices. -``` - -Dependency updates marked `dependencies` do not require a fragment. Other -internal changes may omit one when a maintainer applies `skip-changelog` and -the pull request explains why. - -Towncrier renders the pending fragments for prerelease notes, then collects and -removes them when a stable release is created. Do not edit `CHANGELOG.md` -directly. diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 15d6d11..9d38040 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,3 +1,5 @@ # Changelog -Notable changes to the VidXP desktop application are recorded here. +## 0.2.1-b.1 + +Initial desktop preview. diff --git a/desktop/VERSION b/desktop/VERSION new file mode 100644 index 0000000..b0eba74 --- /dev/null +++ b/desktop/VERSION @@ -0,0 +1 @@ +0.2.1-b.1 diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index fc57db1..4ca0eda 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -4373,7 +4373,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vidxp-desktop" -version = "0.2.1-b.1" +version = "0.2.1-b.1" # x-release-please-version dependencies = [ "atomic-write-file", "hex", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 9daecd0..846c719 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vidxp-desktop" -version = "0.2.1-b.1" +version = "0.2.1-b.1" # x-release-please-version description = "The VidXP desktop launcher and local runtime supervisor" edition = "2024" rust-version = "1.97" diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index c831e8d..2a62b76 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1603,19 +1603,23 @@ mod tests { #[test] fn package_specification_has_one_sorted_extra_set() { let manifest = manifest().expect("manifest"); + let version = &manifest.package_version; - assert_eq!(base_package_specification(&manifest), "vidxp==0.2.1-b.1"); + assert_eq!( + base_package_specification(&manifest), + format!("vidxp=={version}") + ); assert_eq!( package_specification( &manifest, &["scene".into(), "dialogue".into()], &["browser".into()], ), - "vidxp[dialogue,frontend,scene]==0.2.1-b.1" + format!("vidxp[dialogue,frontend,scene]=={version}") ); assert_eq!( package_specification(&manifest, &["scene".into()], &[]), - "vidxp[scene]==0.2.1-b.1" + format!("vidxp[scene]=={version}") ); assert_eq!( selected_surfaces(&manifest, &["browser".into(), "browser".into()]) diff --git a/docs/releasing.md b/docs/releasing.md index eaf514e..f037022 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,34 +1,73 @@ # Release process -VidXP publishes prerelease and stable packages from `main`. The old `release` -branch is retained only as historical ancestry for `v0.1.0`; it is not an active -publication branch. - -## Prereleases - -A release-relevant merge to `main` runs CI, lets Python Semantic Release -calculate the next version, creates a `b` prerelease tag and GitHub prerelease, -builds the distributions, and publishes them to TestPyPI. Commits that do not -require a semantic version bump do not publish a package. - -Towncrier renders the pending fragments into the GitHub prerelease body without -consuming them. The fragments remain in `changes/` for the stable release. - -## Stable releases - -1. Confirm that `main` is green and its TestPyPI prerelease is usable. -2. Confirm that every user-visible merged pull request has an accurate fragment. -3. Run the **Release (main → PyPI)** workflow from `main`. -4. Approve the `pypi` environment deployment when reviewer protection is enabled. -5. Confirm the new tag, GitHub release, PyPI package, and emptied pending - fragment set. - -The workflow only runs from a `main` dispatch, and every publication job uses -the release commit created from that immutable revision. Python Semantic -Release is the only version authority. Towncrier is the only release-note -renderer. During the stable build it receives the calculated version, renders -the GitHub release body, updates `CHANGELOG.md` with the same section, and -removes the released fragments before the release commit and tag are created. - -Release and CI tools are declared once in `utils/build-requirements.txt`. Do not -duplicate their versions in workflow files. +VidXP publishes two independently versioned components: + +| Component | Tag | Beta artifacts | Stable artifacts | +|---|---|---|---| +| Core | `v` | TestPyPI and `beta` GHCR images | PyPI and versioned/`latest` GHCR images | +| Desktop | `desktop-v` | Prerelease installers | Stable installers | + +`main` is the default integration branch and the beta channel. `release` is +the protected stable channel. + +## Beta channel + +Feature and fix pull requests target `main`. Release Please maintains a beta +release pull request on that branch from Conventional Commits. + +Merging the beta release pull request: + +1. updates only the changed component versions and changelogs; +2. creates immutable prerelease tags and draft GitHub releases; +3. publishes a changed core to TestPyPI and the `beta` product, control, and + worker images to GHCR; +4. builds changed desktop installers; and +5. exposes each GitHub prerelease only after its complete artifact set exists. + +The desktop runtime manifest pins the selected core package version. A core +release updates that pin, but a new desktop installer is published only when +the desktop component also has a releasable change. + +## Promote beta to stable + +The **Maintain stable promotion PR** workflow keeps one draft pull request from +`main` to `release` whenever the branches differ. Closing it without merging +causes a fresh draft to be created while there are still commits to promote. + +To promote: + +1. freeze merges to `main` for the short promotion window; +2. mark the `main` → `release` draft ready and merge it; +3. review the stable Release Please pull request created on `release`; and +4. merge that pull request to publish stable artifacts. + +A stable core release runs the complete Python and provider matrix, builds and +smokes the distribution, publishes it to PyPI, then builds and smokes all +three GHCR images. Desktop releases build Windows, macOS, and Linux installers. +GitHub releases remain drafts until their complete artifact set is public. + +After all stable releases created from that commit are public, +**Synchronize release channels** fast-forwards `main` to `release`. It refuses +to overwrite new work on `main`. The scheduled run repairs a missed +synchronization after the publication state becomes complete. + +## Publication integrity and retries + +Registry publication is version-locked: + +- an absent Python version is published normally; +- an existing Python version is accepted only when its complete filename and + SHA-256 set exactly matches the distribution built from the immutable tag; +- an existing versioned container tag is accepted only when its source revision + label matches the immutable release tag; and +- any conflict stops publication before moving channel tags or exposing the + GitHub release. + +For an infrastructure failure, rerun the failed jobs from the same workflow +run. If a workflow fix is required, merge the fix to the channel branch and +dispatch the publisher again with the existing tag. Verified matching +artifacts are reused; conflicting artifacts fail instead of being skipped. + +If version selection or changelog content is wrong, do not merge the Release +Please pull request. Correct the commits or configuration and let Release +Please update it. diff --git a/pyproject.toml b/pyproject.toml index af3a404..23cfc77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,63 +136,3 @@ torch = [ name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true - -[tool.semantic_release] -version_toml = ["pyproject.toml:project.version"] -tag_format = "v{version}" -build_command = "bash ./utils/build_package.sh" -build_command_env = ["BUILD_CHANGELOG", "BUILD_RELEASE_NOTES"] -assets = ["CHANGELOG.md"] -commit_parser = "conventional" -major_on_zero = true -allow_zero_version = true - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf"] -parse_squash_commits = true -ignore_merge_commits = true - -[tool.semantic_release.branches.main] -match = "main" -prerelease = false -prerelease_token = "b" - -[tool.towncrier] -directory = "changes" -filename = "CHANGELOG.md" -start_string = "" -title_format = "## v{version} ({project_date})" -issue_format = "[#{issue}](https://github.com/grayhatdevelopers/vidxp/pull/{issue})" -wrap = false - -[[tool.towncrier.type]] -directory = "breaking" -name = "Breaking Changes" -showcontent = true - -[[tool.towncrier.type]] -directory = "feature" -name = "Features" -showcontent = true - -[[tool.towncrier.type]] -directory = "bugfix" -name = "Bug Fixes" -showcontent = true - -[[tool.towncrier.type]] -directory = "deprecation" -name = "Deprecations" -showcontent = true - -[[tool.towncrier.type]] -directory = "docs" -name = "Documentation" -showcontent = true - -[[tool.towncrier.type]] -directory = "security" -name = "Security" -showcontent = true diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..fc92393 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "61e8b6f8374f3fa3b8c4012e499b4064d8f5c4d2", + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": false, + "draft": true, + "force-tag-creation": true, + "group-pull-request-title-pattern": "chore(release): prepare ${branch}", + "include-v-in-tag": true, + "prerelease": true, + "separate-pull-requests": false, + "versioning": "prerelease", + "plugins": [ + { + "type": "linked-versions", + "groupName": "VidXP", + "components": [ + "vidxp", + "desktop" + ] + } + ], + "packages": { + ".": { + "component": "vidxp", + "exclude-paths": [ + "desktop" + ], + "extra-files": [ + { + "jsonpath": "$.package_version", + "path": "desktop/runtime-manifest.json", + "type": "json" + } + ], + "include-component-in-tag": false, + "package-name": "vidxp", + "prerelease-type": "b", + "release-type": "python" + }, + "desktop": { + "component": "desktop", + "extra-files": [ + { + "path": "src-tauri/Cargo.toml", + "type": "generic" + }, + { + "path": "src-tauri/Cargo.lock", + "type": "generic" + }, + { + "jsonpath": "$.version", + "path": "package.json", + "type": "json" + }, + { + "jsonpath": "$.version", + "path": "package-lock.json", + "type": "json" + }, + { + "jsonpath": "$.packages[''].version", + "path": "package-lock.json", + "type": "json" + }, + { + "jsonpath": "$.desktop_version", + "path": "runtime-manifest.json", + "type": "json" + }, + { + "jsonpath": "$.version", + "path": "src-tauri/tauri.conf.json", + "type": "json" + } + ], + "include-component-in-tag": true, + "package-name": "vidxp-desktop", + "prerelease-type": "b", + "release-type": "simple", + "version-file": "VERSION" + } + } +} diff --git a/release-please-config.stable.json b/release-please-config.stable.json new file mode 100644 index 0000000..dfb9d38 --- /dev/null +++ b/release-please-config.stable.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "61e8b6f8374f3fa3b8c4012e499b4064d8f5c4d2", + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": false, + "draft": true, + "force-tag-creation": true, + "group-pull-request-title-pattern": "chore(release): prepare ${branch}", + "include-v-in-tag": true, + "separate-pull-requests": false, + "plugins": [ + { + "type": "linked-versions", + "groupName": "VidXP", + "components": [ + "vidxp", + "desktop" + ] + } + ], + "packages": { + ".": { + "component": "vidxp", + "exclude-paths": [ + "desktop" + ], + "extra-files": [ + { + "jsonpath": "$['.']", + "path": ".release-please-manifest.json", + "type": "json" + }, + { + "jsonpath": "$.desktop", + "path": ".release-please-manifest.json", + "type": "json" + }, + { + "jsonpath": "$.package_version", + "path": "desktop/runtime-manifest.json", + "type": "json" + } + ], + "include-component-in-tag": false, + "package-name": "vidxp", + "release-type": "python" + }, + "desktop": { + "component": "desktop", + "extra-files": [ + { + "path": "src-tauri/Cargo.toml", + "type": "generic" + }, + { + "path": "src-tauri/Cargo.lock", + "type": "generic" + }, + { + "jsonpath": "$.version", + "path": "package.json", + "type": "json" + }, + { + "jsonpath": "$.version", + "path": "package-lock.json", + "type": "json" + }, + { + "jsonpath": "$.packages[''].version", + "path": "package-lock.json", + "type": "json" + }, + { + "jsonpath": "$.desktop_version", + "path": "runtime-manifest.json", + "type": "json" + }, + { + "jsonpath": "$.version", + "path": "src-tauri/tauri.conf.json", + "type": "json" + } + ], + "include-component-in-tag": true, + "package-name": "vidxp-desktop", + "release-type": "simple", + "version-file": "VERSION" + } + } +} diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 75b12be..9090c21 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -329,6 +329,158 @@ def test_desktop_manifest_matches_published_package_contract(self): "system", ) + def test_release_please_preserves_desktop_manifests_and_links_versions(self): + for filename in ( + "release-please-config.json", + "release-please-config.stable.json", + ): + config = json.loads( + (ROOT / filename).read_text(encoding="utf-8") + ) + linked_versions = [ + plugin + for plugin in config["plugins"] + if plugin["type"] == "linked-versions" + ] + self.assertEqual(len(linked_versions), 1, filename) + self.assertEqual( + set(linked_versions[0]["components"]), + {"vidxp", "desktop"}, + filename, + ) + + desktop = config["packages"]["desktop"] + self.assertEqual(desktop["version-file"], "VERSION", filename) + generic_files = { + extra["path"] + for extra in desktop["extra-files"] + if extra["type"] == "generic" + } + self.assertIn("src-tauri/Cargo.toml", generic_files, filename) + self.assertIn("src-tauri/Cargo.lock", generic_files, filename) + + stable = json.loads( + (ROOT / "release-please-config.stable.json").read_text( + encoding="utf-8" + ) + ) + beta_manifest_updates = { + (entry["path"], entry.get("jsonpath")) + for package in stable["packages"].values() + for entry in package["extra-files"] + if entry["path"] == ".release-please-manifest.json" + } + self.assertEqual( + beta_manifest_updates, + { + (".release-please-manifest.json", "$['.']"), + (".release-please-manifest.json", "$.desktop"), + }, + ) + + manifest = json.loads( + (ROOT / "desktop" / "runtime-manifest.json").read_text( + encoding="utf-8" + ) + ) + package = json.loads( + (ROOT / "desktop" / "package.json").read_text(encoding="utf-8") + ) + cargo = tomllib.loads( + ( + ROOT / "desktop" / "src-tauri" / "Cargo.toml" + ).read_text(encoding="utf-8") + ) + version_file = ( + ROOT / "desktop" / "VERSION" + ).read_text(encoding="utf-8").strip() + self.assertEqual(version_file, manifest["desktop_version"]) + self.assertEqual(version_file, package["version"]) + self.assertEqual(version_file, cargo["package"]["version"]) + self.assertIn( + f'version = "{version_file}" # x-release-please-version', + ( + ROOT / "desktop" / "src-tauri" / "Cargo.toml" + ).read_text(encoding="utf-8"), + ) + self.assertNotIn( + f"vidxp=={version_file}", + ( + ROOT / "desktop" / "src-tauri" / "src" / "lib.rs" + ).read_text(encoding="utf-8"), + ) + + build_command = "bash utils/build_package.sh" + for workflow in ( + ".github/workflows/ci.yml", + ".github/workflows/release-to-test-pypi.yml", + ".github/workflows/release-to-pypi.yml", + ): + self.assertIn( + build_command, + (ROOT / workflow).read_text(encoding="utf-8"), + workflow, + ) + + release_workflow = ( + ROOT / ".github" / "workflows" / "release-please.yml" + ).read_text(encoding="utf-8") + self.assertEqual(release_workflow.count('--ref "$TAG"'), 2) + self.assertEqual( + release_workflow.count('--repo "$GITHUB_REPOSITORY"'), + 2, + ) + self.assertNotIn( + '--ref "${{ github.ref_name }}"', + release_workflow, + ) + + desktop_publish = ( + ROOT / ".github" / "workflows" / "publish-desktop.yml" + ).read_text(encoding="utf-8") + self.assertIn('"VERSION":', desktop_publish) + self.assertIn('"runtime manifest package":', desktop_publish) + self.assertEqual(desktop_publish.count("--latest"), 1) + core_publish = ( + ROOT / ".github" / "workflows" / "release-to-pypi.yml" + ).read_text(encoding="utf-8") + self.assertNotIn("--latest", core_publish) + + for workflow in ("ci.yml", "desktop.yml", "security.yml"): + contents = ( + ROOT / ".github" / "workflows" / workflow + ).read_text(encoding="utf-8") + self.assertIn(" - release", contents, workflow) + self.assertIn("github.base_ref != 'release'", contents, workflow) + self.assertIn("github.head_ref != 'main'", contents, workflow) + desktop_ci = ( + ROOT / ".github" / "workflows" / "desktop.yml" + ).read_text(encoding="utf-8") + self.assertIn( + "!startsWith(github.head_ref, 'release-please--branches--')", + desktop_ci, + ) + + promotion = ( + ROOT / ".github" / "workflows" / "promotion-pr.yml" + ).read_text(encoding="utf-8") + self.assertIn('"$status" == "diverged"', promotion) + synchronization = ( + ROOT / ".github" / "workflows" / "sync-channels.yml" + ).read_text(encoding="utf-8") + self.assertIn( + 'git push origin "$publication_sha:refs/heads/main"', + synchronization, + ) + self.assertNotIn( + 'git push origin "origin/release:refs/heads/main"', + synchronization, + ) + self.assertIn( + '--title "chore(release): synchronize stable baseline"', + synchronization, + ) + def test_windows_release_binary_uses_the_gui_subsystem(self): main = ( ROOT / "desktop" / "src-tauri" / "src" / "main.rs" diff --git a/utils/build-requirements.txt b/utils/build-requirements.txt index f8e77d3..32e0937 100644 --- a/utils/build-requirements.txt +++ b/utils/build-requirements.txt @@ -1,5 +1,3 @@ beautifulsoup4~=4.15.0 build~=1.5.0 -python-semantic-release~=10.6.1 ruff~=0.16.0 -towncrier~=25.8.0 diff --git a/utils/build_package.sh b/utils/build_package.sh index 70a8495..5f7d68d 100644 --- a/utils/build_package.sh +++ b/utils/build_package.sh @@ -6,7 +6,11 @@ README="README.md" README_BAK="$README.bak" BUILD_DIR="build" DIST_DIR="dist" -RELEASE_NOTES=".release-notes.md" + +if [[ -z "${SOURCE_DATE_EPOCH:-}" ]]; then + SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)" + export SOURCE_DATE_EPOCH +fi restore_readme() { if [[ -f "$README_BAK" ]]; then @@ -17,29 +21,7 @@ restore_readme() { trap restore_readme EXIT echo "🧹 Removing stale package artifacts..." -rm -rf -- "$BUILD_DIR" "$DIST_DIR" "$RELEASE_NOTES" - -if [[ "${BUILD_CHANGELOG:-0}" == "1" || "${BUILD_RELEASE_NOTES:-0}" == "1" ]]; then - if [[ -z "${NEW_VERSION:-}" ]]; then - echo "NEW_VERSION is required when rendering release notes" >&2 - exit 1 - fi - - RELEASE_DATE="$(date -u +%Y-%m-%d)" - echo "📰 Rendering release notes for v${NEW_VERSION}..." - towncrier build \ - --draft \ - --version "$NEW_VERSION" \ - --date "$RELEASE_DATE" > "$RELEASE_NOTES" -fi - -if [[ "${BUILD_CHANGELOG:-0}" == "1" ]]; then - echo "📰 Building changelog for v${NEW_VERSION}..." - towncrier build \ - --yes \ - --version "$NEW_VERSION" \ - --date "$RELEASE_DATE" -fi +rm -rf -- "$BUILD_DIR" "$DIST_DIR" echo "📝 Backing up original README..." cp "$README" "$README_BAK" @@ -50,6 +32,26 @@ python utils/fix_readme_links.py "$BASE_URL" "$README" --inplace echo "📦 Building package..." python -m build +for sdist in "$DIST_DIR"/*.tar.gz; do + archive_listing="$(tar -tzf "$sdist")" + archive_root="${archive_listing%%/*}" + normalize_dir="$(mktemp -d)" + uncompressed="${sdist%.gz}" + tar -xzf "$sdist" -C "$normalize_dir" + rm -- "$sdist" + tar \ + --sort=name \ + --mtime="@${SOURCE_DATE_EPOCH}" \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + -cf "$uncompressed" \ + -C "$normalize_dir" \ + "$archive_root" + gzip --no-name --best "$uncompressed" + rm -rf -- "$normalize_dir" +done + echo "♻️ Restoring original README..." restore_readme diff --git a/utils/verify_published_distribution.py b/utils/verify_published_distribution.py new file mode 100644 index 0000000..3cac159 --- /dev/null +++ b/utils/verify_published_distribution.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Verify whether a Python distribution version is absent or byte-identical.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +import sys +from urllib.error import HTTPError +from urllib.parse import quote +from urllib.request import urlopen + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as file: + for chunk in iter(lambda: file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repository", required=True) + parser.add_argument("--package", required=True) + parser.add_argument("--version", required=True) + parser.add_argument("--dist", type=Path, default=Path("dist")) + args = parser.parse_args() + + local = { + path.name: sha256(path) + for path in args.dist.iterdir() + if path.is_file() + } + if not local: + raise SystemExit(f"No distribution files found in {args.dist}") + + url = ( + f"{args.repository.rstrip('/')}/pypi/" + f"{quote(args.package)}/{quote(args.version)}/json" + ) + try: + with urlopen(url, timeout=30) as response: # noqa: S310 + payload = json.load(response) + except HTTPError as error: + if error.code == 404: + print("absent") + return 0 + raise + + remote = { + file["filename"]: file["digests"]["sha256"] + for file in payload.get("urls", []) + } + if local == remote: + print("identical") + return 0 + + missing = sorted(local.keys() - remote.keys()) + extra = sorted(remote.keys() - local.keys()) + changed = sorted( + filename + for filename in local.keys() & remote.keys() + if local[filename] != remote[filename] + ) + print( + "Published distribution conflicts with this tag: " + f"missing={missing}, extra={extra}, changed={changed}", + file=sys.stderr, + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 8d4d5effda8b00a113315a287c31490a43b212d0 Mon Sep 17 00:00:00 2001 From: Talha Date: Fri, 31 Jul 2026 01:49:50 +0500 Subject: [PATCH 6/8] docs: present product setup capabilities and roadmap --- INSTALLATION_GUIDE.md | 507 ++++++-- README.md | 329 +++-- docs/CONTRIBUTING.md | 311 +++-- docs/architecture/platform.md | 1505 +++++++++++++++++++++++ docs/benchmarking/README.md | 19 +- docs/benchmarking/adapter_validation.md | 165 ++- docs/benchmarking/benchmark_catalog.md | 15 +- docs/benchmarking/core_contract.md | 70 +- docs/benchmarking/research_papers.md | 2 +- docs/benchmarking/results.md | 74 +- docs/benchmarking/runtime_validation.md | 42 +- docs/benchmarking_research.md | 10 +- docs/desktop.md | 145 +++ 13 files changed, 2699 insertions(+), 495 deletions(-) create mode 100644 docs/architecture/platform.md create mode 100644 docs/desktop.md diff --git a/INSTALLATION_GUIDE.md b/INSTALLATION_GUIDE.md index fc5c747..d8ddca4 100644 --- a/INSTALLATION_GUIDE.md +++ b/INSTALLATION_GUIDE.md @@ -1,218 +1,489 @@ # Installation guide -The main [README](README.md) introduces the product and contains the shortest -install-and-run path. This guide covers platform prerequisites, source -installation, model preparation, and common first-run issues. +Use this guide to choose one supported VidXP shape and install only what that +shape needs. -## Prerequisites +## Choose an installation -- Python 3.10 through 3.13 -- FFmpeg available on `PATH` when installing `dialogue` -- CMake and a C/C++ build toolchain when installing `actor` +| Goal | Recommended installation | What runs | +|---|---|---| +| Native CLI indexing | `vidxp[local-worker]` | CLI plus a supervised local worker | +| Local agent integration | `vidxp[local-worker,mcp]` | Local worker and stdio MCP | +| Native browser UI | `vidxp[local-worker,frontend]` | CLI, local worker, Streamlit | +| Local application server | `vidxp[local-worker,server]` | Loopback HTTP API, remote MCP, local worker | +| Desktop app | Install the native package | Guided app-owned Python and worker runtime with an optional browser interface | +| Browser UI in Docker | Published `vidxp` image | One CPU worker/UI container | +| Public/self-hosted service | `compose.coolify.yaml` | API/MCP control plane, CPU worker, PostgreSQL, Chroma, tusd | +| Embed one capability | `dialogue`, `scene`, or `actor` extra | Python indexing/retrieval code | -The official `dlib` package is distributed through PyPI as source. Installing -VidXP therefore compiles it locally unless a compatible build is already cached -or provided by the environment. Changing from Python 3.13 to 3.11 does not -remove that requirement. +Do not install the bare package and expect it to index video. Base `vidxp` +provides the lightweight command shell, configuration, and typed contracts. +Local model work requires a capability or worker extra. -On Windows, install CMake and the Microsoft C++ Build Tools. Do not add -platform-specific `dlib` wheels to this repository. +## What your machine needs -## Create an environment +| Installation | Install first | Managed by VidXP | +|---|---|---| +| CLI or MCP | [uv 0.12+](https://docs.astral.sh/uv/getting-started/installation/) | Python and the isolated VidXP environment | +| Desktop | A supported OS and internet access for first setup | uv, Python, VidXP, and selected model files | +| Docker | Docker Engine or Docker Desktop | Python, VidXP, and FFmpeg inside the image | -Using a virtual environment keeps VidXP and its Python dependencies separate -from the system Python: +Native CLI and desktop processing require FFmpeg, ffprobe, `libx264`, and +`aac`. `vidxp init` checks them and offers the supported operating-system +package-manager command when something is missing. The desktop app performs +the same check through native confirmation dialogs. Docker already includes +FFmpeg. + +Supported native systems are Windows x86-64, Linux x86-64, and Apple Silicon +macOS 14 or newer. CPU is the supported runtime; GPU installation remains +deferred. + +## Native local setup + +uv installs VidXP as an isolated command-line tool, selects Python, and +resolves CPU PyTorch. Users do not need to create or activate a virtual +environment. + +### 1. Install the profile you need ```bash -python -m venv venv +# Check uv +uv --version + +# Install CLI and MCP +uv tool install --python 3.14 --torch-backend cpu "vidxp[local-worker,mcp]" ``` -```bash -# Windows -venv\Scripts\activate +Replace the bracketed extras when you need another surface: -# macOS/Linux -source venv/bin/activate +| Goal | Install this package profile | +|---|---| +| CLI indexing | `vidxp[local-worker]` | +| Browser UI | `vidxp[local-worker,frontend]` | +| Local stdio MCP | `vidxp[local-worker,mcp]` | +| Local API + remote MCP | `vidxp[local-worker,server]` | +| UI + API + both MCP transports | `vidxp[local-worker,frontend,server]` | + +For example, add the browser interface with: + +```bash +uv tool install --force --python 3.14 --torch-backend cpu \ + "vidxp[local-worker,mcp,frontend]" ``` -Upgrade pip before installing: +`--torch-backend cpu` prevents an unintended CUDA dependency set on Linux and +keeps the supported runtime consistent across platforms. If `vidxp` is not +found after installation, run `uv tool update-shell` and reopen the terminal. + +### 2. Initialize FFmpeg ```bash -python -m pip install --upgrade pip +vidxp init ``` -## Install from PyPI +`init` performs these steps: + +1. resolves FFmpeg and ffprobe; +2. executes version checks; +3. verifies the `libx264` and `aac` encoders; +4. shows the exact supported package-manager command if anything is missing; +5. asks before running WinGet or Homebrew; and +6. stores the verified absolute paths in VidXP’s per-user configuration. -Install the lightweight command and application layer: +Linux prints an applicable APT, DNF, or pacman command for the user to run in a +system terminal. VidXP does not automate `sudo`. + +For scripts and CI: ```bash -python -m pip install vidxp +vidxp init --json \ + --ffmpeg /absolute/path/to/ffmpeg \ + --ffprobe /absolute/path/to/ffprobe ``` -Add only the indexing capabilities you need: +Redirected/noninteractive input never prompts or installs. `--yes` is an +explicit authorization to run the displayed supported package-manager command. + +The lightweight one-off form is: ```bash -python -m pip install "vidxp[dialogue]" -python -m pip install "vidxp[scene,actor]" -python -m pip install "vidxp[all]" +uvx vidxp init ``` -`all` contains all runtime capabilities. It does not include the `frontend` or -`benchmarks` extras: +Because the paths are stored in per-user configuration, a later permanent +installation can reuse initialization performed by `uvx`. + +### 3. Prepare models ```bash -python -m pip install "vidxp[all,frontend]" +vidxp prepare ``` -The `benchmarks` extra contains evaluation tooling only. Combine it with the -capability being evaluated, for example `vidxp[scene,benchmarks]`. +The command displays the models that are missing, their pinned download sizes, +the cache location, and the maximum additional disk use before asking for +confirmation. -## Install from source +| Capability | Models | Approximate download | +|---|---|---:| +| Dialogue | Qwen3 Embedding 0.6B + faster-whisper large-v3-turbo | 2.64 GiB | +| Scene | SigLIP2 base patch16-224 | 1.43 GiB | +| Actor | OpenCV Zoo YuNet + SFace | 37 MiB | -From the repository root: +Prepare only what you plan to index: ```bash -python -m pip install . +vidxp prepare --modalities scene +vidxp prepare --modalities dialogue,actor ``` -Include all capabilities and the Streamlit interface: +Noninteractive preparation requires explicit confirmation: ```bash -python -m pip install ".[all,frontend]" +vidxp prepare --modalities scene --yes ``` -Use an editable installation while developing: +Indexing, API jobs, and MCP tools never turn the first request into a hidden +model download. + +#### Upgrading from the previous model stack + +This release replaces: + +- WhisperX `large-v2` and `all-MiniLM-L6-v2` with faster-whisper + `large-v3-turbo` and Qwen3 Embedding 0.6B; +- OpenAI CLIP `ViT-B/32` with SigLIP2 base patch16-224; and +- `face_recognition`/dlib with OpenCV Zoo YuNet and SFace. + +The resulting embeddings, actor thresholds, and provider manifests are not +compatible with indexes built by the earlier stack. Prepare the new models, +then re-index the registered videos you want in the active snapshot. Keep the +old repository until the replacement index has been validated. + +### 4. Verify ```bash -python -m pip install -e ".[all,frontend,benchmarks]" +vidxp doctor ``` -## Run the container +`doctor` checks installed distributions, provider imports, FFmpeg, codecs, and +the selected pinned model artifacts. It never installs packages, changes the +operating system, constructs models, or downloads weights. -Stable container images include every runtime capability and the browser -interface, but not downloaded model weights. Docker Compose keeps indexes, -repository configuration, and model caches in the `vidxp-data` volume: +Limit the check when you prepared a subset: ```bash -docker compose pull -docker compose run --rm vidxp vidxp prepare -docker compose up +vidxp doctor --modalities scene +vidxp doctor --modalities dialogue,actor ``` -The interface is available at `http://localhost:8501`. Set `VIDXP_PORT` in a -local `.env` file to publish a different host port. Set `VIDXP_IMAGE` to select -an immutable release instead of `latest`: +### 5. Start the selected surface -```dotenv -VIDXP_IMAGE=ghcr.io/grayhatdevelopers/vidxp:0.2.0 -VIDXP_PORT=8502 -VIDXP_DEVICE=cpu -``` +- CLI: `vidxp --help` +- Browser UI: `vidxp ui` +- Local HTTP API and remote MCP: `vidxp-api` +- Local stdio MCP: `vidxp-mcp` -Run the image without Compose when persistent storage is not required: +## First CLI index ```bash -docker run --rm -p 8501:8501 ghcr.io/grayhatdevelopers/vidxp:latest +vidxp media import samplevideo.mp4 --json +vidxp index create +vidxp search scene "a yellow taxi on a city street" ``` -## Verify the installation +- `media import` copies and validates the video in managed storage. +- `media list` rediscovers registered filenames and IDs. +- `index list` shows active searchable membership. +- Search/query without `--media-id` ranks across every indexed video. +- `--media-id ` restricts results to one video. -Display the installed package version: +Index one capability or change scene cadence: ```bash -vidxp --version +vidxp index create \ + --modality scene \ + --scene-sample-fps 1 ``` -Check the Python and system dependencies needed by all indexing capabilities: +## Optional dependency extras + +Extras are composable: + +| Extra | Includes | Does not include | +|---|---|---| +| `storage` | Embedded Chroma and host monitoring | Model providers or UI | +| `dialogue` | Storage, transcription, dialogue embeddings | Scene/actor providers | +| `scene` | Storage, PyTorch, Transformers, OpenCV, Pillow | Dialogue/actor providers | +| `actor` | Storage, OpenCV, YuNet/SFace support | Dialogue/scene providers | +| `all` | Dialogue, scene, and actor | Grounded-query model client and UI | +| `local-worker` | `all` plus grounded-query client | Browser UI, MCP SDK, HTTP server | +| `frontend` | Streamlit | Worker providers | +| `mcp` | MCP SDK | Worker providers | +| `slm` | OpenAI-compatible local query-model client | A language model or model weights | +| `server` | FastAPI, remote MCP, PostgreSQL/control dependencies | Local providers | +| `server-worker` | Server storage client and every CPU provider | Public API process | +| `benchmarks` | Benchmark adapter dependencies | Local worker providers | +| `test` | Pytest and HTTP test client | Product runtime features | + +The `server` extra is intentionally model-free. Use it with `local-worker` for +a loopback all-in-one API, or use the separate control and worker images for a +deployed server. + +## Local MCP + +After installing `local-worker,mcp`: ```bash -vidxp doctor +vidxp mcp-config ``` -`vidxp doctor` imports the selected dependencies but does not download model -weights. A base-only install therefore reports which capability extras are -missing. Restrict the check when diagnosing one capability: +The command prints a complete, import-ready `mcpServers` JSON object with the +resolved absolute `vidxp-mcp` executable and default repository argument. ```bash -vidxp doctor --modalities scene +vidxp-mcp --check --repository default ``` -If the frontend extra was installed, start the interface with `vidxp ui`. It -uses the active repository configured by `vidxp repositories use` and runs -until stopped with `Ctrl+C`. +The self-check performs a real stdio handshake, discovers tools, calls the +read-only index-status tool, prints resolved data/index paths, and exits. + +Useful alternatives: -## Prepare models +| Command | Result | +|---|---| +| `vidxp mcp-config --repository ` | Client JSON for a named repository | +| `vidxp-mcp --print-config` | JSON without other output | +| `vidxp-mcp --help` | Options plus a copy/paste example | + +## Local HTTP API -Model weights are not part of the Python package: +Install `local-worker,server`, prepare models, then run: -| Capability | Model | +```bash +vidxp-api +``` + +The unauthenticated local default is deliberately loopback-only: + +| Endpoint | Purpose | |---|---| -| Dialogue embeddings | `sentence-transformers/all-MiniLM-L6-v2` | -| Transcription | WhisperX `large-v2` | -| Scene search | CLIP `ViT-B/32` | -| Word alignment | WhisperX model selected for the detected language | +| `http://127.0.0.1:8000/docs` | Interactive OpenAPI | +| `http://127.0.0.1:8000/openapi.json` | Machine-readable contract | +| `http://127.0.0.1:8000/health` | Process liveness | +| `http://127.0.0.1:8000/ready` | Aggregate runtime readiness | +| `http://127.0.0.1:8000/mcp` | Streamable HTTP MCP | + +Do not bind an unauthenticated API to a non-loopback address. Public +deployments require static bearer or OIDC authentication and should use the +supported server Compose topology. + +## Desktop application + +Download the Windows, Apple Silicon macOS, or Linux package from +[GitHub Releases](https://github.com/grayhatdevelopers/vidxp/releases). -Download and cache the fixed dialogue, transcription, and scene models before -indexing: +On first launch, the application checks FFmpeg, provisions its own Python and +VidXP runtime, lets you choose capabilities and model storage, and optionally +downloads the selected models. Python and uv do not need to be installed +separately. + +Capability code is selected independently from the optional browser interface. +After configuration, the Tauri supervisor stays in the system tray. Closing +the window hides it; **Quit VidXP** from the tray shuts down the interface and +worker. + +The NSIS, DMG, and AppImage packages do not bundle FFmpeg. The application uses +a native confirmation dialog before running a supported package manager. +Windows SmartScreen and macOS Gatekeeper may require explicit confirmation +until signing is added. See [Desktop application](docs/desktop.md) for runtime, +storage, and build details. + +## Docker + +The published local image contains the CPU worker, browser UI, Python, and +FFmpeg. Prepare models into a persistent volume, then start the app: ```bash -vidxp prepare +# Download selected models +docker run --rm -it \ + -v vidxp-data:/var/lib/vidxp \ + ghcr.io/grayhatdevelopers/vidxp:latest \ + vidxp prepare + +# Start the browser app +docker run --rm --init \ + -p 8501:8501 \ + -v vidxp-data:/var/lib/vidxp \ + ghcr.io/grayhatdevelopers/vidxp:latest ``` -WhisperX selects its alignment model after detecting the video's language. Cache -a known language explicitly when required: +Open `http://localhost:8501`. Pin a published version instead of `latest` for +a long-lived installation. + +## Published container images + +Every stable release publishes three Linux/amd64 images from the same validated +release commit: + +| Image | Published tag | Purpose | +|---|---|---| +| Local | ``, `.`, `latest` | CPU worker + browser UI | +| Control | `-control` | API, remote MCP, migrations, private upload hooks | +| Worker | `-worker` | CPU model providers and server storage client | + +Repository: + +```text +ghcr.io/grayhatdevelopers/vidxp +``` + +The local image is used by `compose.yaml`. The control and worker images are +used by `compose.coolify.yaml`. + +## Coolify and server Compose + +The supported server topology contains: + +| Control plane | Execution and storage | Ingestion | +|---|---|---| +| FastAPI + Streamable HTTP MCP | One CPU worker, PostgreSQL, Chroma | tusd + private hook service | + +It is a single-node, single-repository deployment. It does not claim +multi-replica failover, hosted database substitution, or GPU execution. + +Follow [Coolify deployment](docs/deployment/coolify.md) for: + +- exact control/worker image variables; +- required secrets and public hostnames; +- proxy rules for MCP and resumable uploads; +- explicit worker model preparation through the authenticated API or MCP; +- readiness/migration gates; +- persistent volumes and backups; and +- the optional, explicitly prepared Ollama profile. + +## Install from source + +Source installation is for contributors and reproducible development, not the +normal public quick start: + +```bash +git clone https://github.com/grayhatdevelopers/vidxp.git +cd vidxp +uv sync --frozen \ + --extra local-worker \ + --extra frontend \ + --extra mcp \ + --extra server +uv run --no-sync vidxp init +uv run --no-sync vidxp prepare +uv run --no-sync vidxp doctor +uv run --no-sync vidxp ui +``` + +The lock routes PyTorch through the official CPU index on Linux and Windows. + +## Data and configuration locations + +Local commands do not treat the current directory as their storage root. + +| Platform | Data root | Configuration root | +|---|---|---| +| Windows | `%LOCALAPPDATA%\VidXP` | `%APPDATA%\VidXP` | +| macOS | `~/Library/Application Support/VidXP` | `~/Library/Application Support/VidXP` | +| Linux | `${XDG_DATA_HOME:-~/.local/share}/VidXP` | `${XDG_CONFIG_HOME:-~/.config}/VidXP` | + +The data root contains: + +```text +VidXP/ + repositories/ + default/ + models/ +``` + +The desktop application uses this same root for repositories and its default +model cache. Its managed Python environments and active-runtime pointer remain +in the identifier-scoped private application-data directory. On Windows, +per-user program files are installed separately under +`%LOCALAPPDATA%\Programs\VidXP`. + +Use an alternate root for one CLI invocation: ```bash -vidxp prepare --option dialogue.alignment_language=en +vidxp --data-dir /path/to/vidxp-data ui ``` -SentenceTransformer and WhisperX use the Hugging Face cache; CLIP uses its own -user cache. These caches normally live outside the virtual environment, so -removing the environment does not necessarily remove downloaded model weights. +Set `VIDXP_DATA_DIR` when the CLI, UI, local MCP, and local API should all use +the same alternate root. `VIDXP_INDEX_DIR` and `VIDXP_MODEL_CACHE` are advanced +single-location overrides. Docker uses its declared volumes instead. + +## Troubleshooting -## First indexing run +### `vidxp` is not found -A successful installation means the commands and Python dependencies are -available. It does not mean the model weights have been downloaded. +- Run `uv tool update-shell`, restart the shell, and try again. +- Confirm the installation with `uv tool list`. -If `vidxp prepare` was not run first, the initial indexing command downloads any -missing models before processing the video: +### FFmpeg or a codec is missing ```bash -vidxp index create samplevideo.mp4 +vidxp init ``` -Keep the terminal and internet connection open until VidXP reports that the -index is ready. Machine-learning dependencies and model caches can consume -several gigabytes, and the first run takes longer while models are downloaded. -Exact sizes depend on the platform and selected package builds. +Review the displayed package-manager command before approving it. Rerun +`vidxp doctor` afterward. -VidXP saves local progress and the final state in -`chroma_data/index_status.json`. Search is unavailable until indexing completes. -If the process stops early, run indexing again; VidXP clears the incomplete -single-video index before rebuilding it. +### Linux or Windows starts resolving NVIDIA packages -## Common problems +Reinstall the managed tool with the explicit CPU option: -### `dlib` fails to install +```bash +uv tool install --force --python 3.14 --torch-backend cpu \ + "vidxp[local-worker,frontend]" +``` + +Do not use plain `pip install "vidxp[local-worker]"` on Linux unless you have +already installed and constrained the intended CPU PyTorch build. -Confirm that CMake and a working C/C++ compiler are installed and visible from -the active terminal. On Windows, use the Microsoft C++ Build Tools. +### uv cannot hardlink from its cache -### FFmpeg is not found +This warning means the cache and environment are on filesystems that cannot +hardlink to each other. Installation falls back to copying and remains valid. +Suppress the warning when that layout is intentional: -Run `ffmpeg -version` in the same terminal. Install FFmpeg or add its executable -directory to `PATH`, then rerun `vidxp doctor`. +```bash +uv tool install --link-mode copy --python 3.14 --torch-backend cpu \ + "vidxp[local-worker,frontend]" +``` -### A search says the index is not ready +or set `UV_LINK_MODE=copy`. -Wait for the active indexing command to finish. If the previous process ended -or failed, run `vidxp index create` again to rebuild the incomplete local -index. +### A model is missing or the worker is offline -### The first indexing run appears slow +```bash +vidxp prepare +vidxp doctor +``` + +For offline operation, prepare while network access is allowed, then set +`VIDXP_ALLOW_MODEL_DOWNLOADS=false`. + +### Search says the index is not ready + +- Check `vidxp index status`. +- Check `vidxp jobs list` and inspect the failed job. +- Retry indexing after fixing the reported dependency/model error. +- A failed or cancelled generation does not replace the previous active + snapshot. + +### Resetting local data + +VidXP intentionally does not provide an implicit destructive reset. Inspect the +resolved paths first: + +```bash +vidxp repositories show +``` -Check the terminal for model-download or indexing progress. Model preparation, -transcription, scene analysis, and actor detection are separate stages. Use -`vidxp prepare` before indexing or select fewer capabilities with -one or more `--modality` options. +Back up or remove only the specific per-user repository/model directories you +intend to discard. diff --git a/README.md b/README.md index d151fda..fff63e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

    - - logo - + + VidXP logo +

    VidXP

    @@ -10,258 +10,227 @@ Search video by what was said, what appeared on screen, and recurring faces.

    -
    -

    - VidXP is a local-first video indexing and search engine distributed as a Python - package. -

    - -
    -
    -
    You can use it: -
      -
    • From the command line
    • -
    • Through its browser interface
    • -
    • As a desktop app
    • -
    • As an API
    • -
    • As an MCP, with your agents (coming soon ⚡️) -
    • As an indexing and retrieval layer inside another application
    • -
    +

    + A local-first video search engine for people, applications, and AI agents. +

    - Dialogue search · Scene search · Actor grouping · CLI · Browser UI · Python API + Dialogue search · Scene search · Actor grouping

    - - PyPI version - - - Supported Python versions - - - CI status - - - MIT license - - - Discord + + Download VidXP desktop app

    -## Why VidXP - -Finding one moment in a video should not require scrubbing through the entire -timeline. VidXP builds a searchable index from three kinds of evidence: +

    + Windows · Apple Silicon macOS · Linux +

    -- **Dialogue:** semantic search over timestamped transcripts. -- **Scenes:** text-to-frame search. -- **Actors:** groups similar detected faces and exports a highlighted video for a selected cluster. +

    + PyPI version + GHCR container + MIT license + Discord +

    -After the required model weights are available, video processing and search run completely locally, for your privacy and security. +## Find the moment, not the timestamp -Some ideas on how to use VidXP: -- Use it as way to find your favorite relatives in a huge folder of wedding videos (been there, done that) -- Use it in your application, allow users to search videos (an idea: use it alongside a video-editing application) -- Use it as an "understanding" layer so your LLM / agent can understand videos +VidXP makes one video—or an entire collection—searchable by meaning: -[![Video Screenshot](./docs/images/video-screenshot.jpeg)](https://www.linkedin.com/feed/update/urn:li:activity:7343569473720725505/) +- **Dialogue search:** type what you remember someone saying and jump to the + matching moments. +- **Scene search:** describe what appeared on screen and find the closest + visual matches. +- **Actor matching:** find recurring faces within a video and export a + highlighted video for a selected group. +Use it to search years of family videos, add video search to an editing +workflow, or let an AI agent answer questions using evidence from your own +video library. Your videos can stay on your machine. -## Current capabilities +[![VidXP browser interface](./docs/images/video-screenshot.jpeg)](https://www.linkedin.com/feed/update/urn:li:activity:7343569473720725505/) -| Capability | Available now | Result | -|---|---|---| -| Dialogue search | Transcription, word alignment, semantic phrase indexing | Matching video time | -| Scene search | Text search over sampled video frames | Matching frame and time | -| Actor grouping | Within-video face detection and clustering | Clustered detections and highlighted output video | -| Interfaces | Typer CLI, Streamlit browser interface, Python API | Interactive or programmatic use | -| Index management | Saved progress, ready/failed state, cancellation, isolated programmatic runs | Traceable and reusable indexes | +## Start here -## Quick start +Choose the setup that fits how you want to use VidXP. -VidXP supports Python 3.10 through 3.13 and requires FFmpeg. See the -[installation guide](INSTALLATION_GUIDE.md) for the `dlib` compiler -requirements, source installation, model preparation, and troubleshooting. +### 1. CLI and MCP -Install the command line and browser interface with -[pipx](https://packaging.python.org/en/latest/guides/installing-stand-alone-command-line-tools/). -The command is available on your `PATH` while VidXP and its dependencies remain -isolated: +For direct use, scripts, and local AI agents, install +[uv](https://docs.astral.sh/uv/getting-started/installation/), then run: ```bash -pipx install "vidxp[all,frontend]" -``` +# Install the CPU edition +uv tool install --python 3.14 --torch-backend cpu "vidxp[local-worker,mcp]" -Install only the capabilities you need with a smaller selection such as -`pipx install "vidxp[scene,frontend]"` or -`pipx install "vidxp[dialogue,scene]"`. To import VidXP from another Python -project, install it into that project's environment instead; the -[installation guide](INSTALLATION_GUIDE.md) covers that path. +# Set up FFmpeg +vidxp init -Confirm the installed package and its runtime dependencies: +# Download search models +vidxp prepare -```bash -vidxp --version +# Check everything vidxp doctor + +# Connect an MCP client +vidxp mcp-config ``` -The first use of each capability downloads its model weights. Download the fixed -dialogue, transcription, and scene models in advance with: +The CLI works without MCP. Add the browser app with: ```bash -vidxp prepare +uv tool install --python 3.14 --torch-backend cpu \ + "vidxp[local-worker,mcp,frontend]" +vidxp ui ``` -## Index and search +If the `vidxp` command is not found, run `uv tool update-shell` once and reopen +the terminal. -Build an index containing dialogue, scene, and actor information: +### 2. Desktop app -```bash -vidxp index create samplevideo.mp4 -``` +Download the installer for Windows, Apple Silicon macOS, or Linux from +[GitHub Releases](https://github.com/grayhatdevelopers/vidxp/releases). -Search the completed index: +The desktop installer manages Python, VidXP, and the local worker for you. On +first launch, choose the search capabilities you want, where model files should +live, and whether to download them immediately. Python and uv do not need to be +installed separately. -```bash -vidxp search dialogue "the bread just came out of the oven" -vidxp search scene "a yellow taxi on a city street" --top-k 5 -vidxp actors list -vidxp actors render 1 samplevideo.mp4 -``` +After setup, VidXP can stay available from the system tray. The browser +interface is optional and can be left out of the installed VidXP runtime. -Index only selected capabilities or sample fewer visual frames: +### 3. Docker for a server + +Run the published all-in-one image on a home server or another single machine: ```bash -vidxp index create samplevideo.mp4 --modality scene --frame-stride 5 +docker run --rm --init \ + -p 8501:8501 \ + -v vidxp-data:/var/lib/vidxp \ + ghcr.io/grayhatdevelopers/vidxp:latest ``` -Repeat `--modality` to combine `dialogue`, `scene`, and `actor`. -Run `vidxp --help` or any command followed by `--help` for the complete command -reference. +For a long-lived server, pin a published version instead of `latest`. For a +Coolify deployment, use the published `-control` and `-worker` images with +[`compose.coolify.yaml`](compose.coolify.yaml)—no repository build is required. +See the [Coolify guide](docs/deployment/coolify.md) for the complete setup. -Use named repositories to keep index locations and devices centrally -configured: +## What you can do today -```bash -vidxp repositories add team --index-dir ./indexes/team --device cuda --use -vidxp repositories list -``` +- Build a reusable search library from one video or a whole collection. +- Search dialogue by meaning, even when you do not remember the exact words. +- Find visual moments by describing the scene you are looking for. +- Group recurring faces in a video and render a highlighted actor overlay. +- Search one selected video or every video in the active library. +- Open matching timestamps and export downloadable clips and overlays. +- Keep personal, client, or project libraries separate. +- Follow long indexing jobs, cancel them, and keep the last working index if a + later run fails. +- Use the browser app, automate the CLI, connect an MCP agent, or integrate + VidXP into another application. -## Browser interface +## A first search -Install the `frontend` extra and start: +The browser app guides you through importing and indexing. The same flow from +the command line is: ```bash -vidxp ui -``` +# Add a video +vidxp media import samplevideo.mp4 --json -The command uses the active named repository, starts a local Streamlit server, -and remains active until stopped. -The interface can upload a video, start or cancel indexing, restore saved -progress after a page reload, and search the capabilities available in the -completed index. +# Index the returned media ID +vidxp index create -## Container +# Find a visual moment +vidxp search scene "a yellow taxi on a city street" -Stable releases are available from GitHub Container Registry. Start the local -interface with persistent index and model storage by running: - -```bash -docker compose up +# Find something that was said +vidxp search dialogue "the bread just came out of the oven" ``` -See the [installation guide](INSTALLATION_GUIDE.md#run-the-container) for model -preparation, configuration, and direct `docker run` usage. - -## Use VidXP as a Python package - -The programmatic API supports isolated multi-video runs, supplied timestamped -transcripts, resumable per-video checkpoints, and metadata-rich top-k results. - -```python -from vidxp.core import IndexConfig, VideoSource -from vidxp.core.runner import run_index -from vidxp.capabilities.scene.operations import search_scene - -config = IndexConfig( - dataset="my-library", - split="local", - run_id="demo", - enabled_modalities=("scene",), - frame_stride=5, -) - -run_index( - [ - VideoSource(video_id="video-1", path="videos/first.mp4"), - VideoSource(video_id="video-2", path="videos/second.mp4"), - ], - config, -) - -results = search_scene("a person enters a taxi", config=config, top_k=5) -for hit in results.hits: - print(hit.video_id, hit.start, hit.end, hit.score) -``` +Results include the source video, timestamps, match score, and the evidence +used to find the moment. Add `--media-id ` to search only one video. + +Run `vidxp --help` or `vidxp --help` for the full command reference. -The [Python indexing and retrieval contract](docs/benchmarking/core_contract.md) -documents configuration, stored metadata, result fields, and run layout. +## For applications and AI agents -## Recommended specs +Use the Python package to add selected VidXP capabilities directly to an +application, or use the HTTP API when VidXP runs as a service. -> Coming soon +MCP clients can add and discover videos, start indexing, search dialogue and +scenes, ask questions about a library, and create clips or actor overlays. +Local agents can connect over stdio; remote agents can connect to a +self-hosted VidXP server. ---- +- [Python, HTTP, and MCP installation](INSTALLATION_GUIDE.md) +- [Optional capability packages](INSTALLATION_GUIDE.md#optional-dependency-extras) +- [Coolify server setup](docs/deployment/coolify.md) -## Roadmap +## Downloads and storage -VidXP is an evolving beta. We'd love to hear your feedback and where you'd like to see the project go. +First setup downloads only the models needed for the capabilities you select. +VidXP shows the download size and destination before it starts. -| Area | Current foundation | Direction | -|---|---|---| -| Search results | Top result in the CLI; structured top-k Python results | Rich ranked results, metadata, previews, and filtering across interfaces | -| Temporal search | Frame and transcript-phrase timestamps | Better time ranges, scene boundaries, aggregation, and ranking | -| Video collections | One local CLI/UI index; isolated multi-video Python runs | User-facing persistent multi-video libraries and index management | -| Actor workflows | Face clustering and highlighted video export | Cluster browsing, labeling, actor search, and stronger tracking | -| Speaker context | Timestamped dialogue search | Active-speaker detection and links between speech and visible people | -| Product experience | CLI and browser indexing/search | Clearer progress, result navigation, recovery, and long-running job controls | -| Evaluation | DiDeMo and HiREST baselines | Combined and component benchmarks, beginning with a LongVALE pilot | +| Capability | Approximate model download | +|---|---:| +| Dialogue search | 2.64 GiB | +| Scene search | 1.43 GiB | +| Actor matching | 37 MiB | -## Models and local data +Leave additional space for the VidXP runtime, indexes, source videos, and +exported results. -| Capability | Model | +By default, the CLI and desktop app share the same VidXP data directory: + +| Platform | Default location | |---|---| -| Dialogue embeddings | `sentence-transformers/all-MiniLM-L6-v2` | -| Transcription | WhisperX `large-v2` | -| Scene search | CLIP `ViT-B/32` | -| Word alignment | WhisperX model selected for the detected language | +| Windows | `%LOCALAPPDATA%\VidXP` | +| macOS | `~/Library/Application Support/VidXP` | +| Linux | `${XDG_DATA_HOME:-~/.local/share}/VidXP` | + +Docker keeps the same data in the `vidxp-data` volume shown above. -VidXP maintains the standard local CLI/UI index in `chroma_data/`. Starting a -new local indexing run replaces the previous or incomplete local index. Model -caches normally live outside this directory and outside the virtual environment. +## Product roadmap -## Documentation and project links +The next product improvements are focused on: + +- labeling actor groups and matching the same person across different videos; +- more reliable face tracking across angle, lighting, motion, and occlusion; +- connecting visible people with the dialogue they are speaking; +- better search ranking, time ranges, and natural-language questions across a + whole library; +- richer previews, timelines, filters, saved searches, and result playback; +- easier organization for large personal and project video collections; +- faster indexing and supported GPU acceleration; and +- smoother desktop updates, repair, and model management. + +VidXP is in beta. Feedback about search quality, actor workflows, and real +video-library use cases is especially useful. + +## Help and project links - [Installation and troubleshooting](INSTALLATION_GUIDE.md) -- [Benchmarking status and results](docs/benchmarking/README.md) -- [Adding a capability](docs/adding-a-capability.md) +- [Desktop application](docs/desktop.md) +- [Coolify deployment](docs/deployment/coolify.md) - [Changelog](CHANGELOG.md) - [Issue tracker](https://github.com/grayhatdevelopers/vidxp/issues) - [MIT license](LICENSE) - ## Contributing -See [CONTRIBUTING.md](./docs/CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs. AI/vibe-coded PRs welcome! +Contributions are welcome. Read the +[contribution guide](docs/CONTRIBUTING.md) before opening a pull request. ## Credits -Built by Grayhat Developers PVT Ltd. 2026. Maintained by the community. +Built by Grayhat Developers PVT Ltd. and maintained by the community. Email: info@grayhat.studio - + VidXP contributors diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6847f70..7a77c59 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,125 +1,242 @@ -# Contribution Guidelines +# Contributing to VidXP -Thanks for contributing to VidXP (Video eXPlain). +Thanks for helping improve VidXP. Contributions may target the local product, +public interfaces, model capabilities, deployment, documentation, or +benchmarks. -## Project layout +## Before you start -| File / path | Role | -|-------------|------| -| `src/vidxp/cli.py` | Typer commands and installed `vidxp` entry point | -| `src/vidxp/application.py` | Reusable application boundary for CLI and future adapters | -| `src/vidxp/capabilities/` | Capability registry, schemas, operations, dependencies, and optional CLI modules | -| `src/vidxp/repositories.py` | Persistent named local-index configuration | -| `src/vidxp/frontend.py` | Streamlit interface launched by `vidxp ui` | -| `src/vidxp/core/` | Capability-neutral storage, media, run state, and execution contracts | -| `src/vidxp/benchmarks/` | Benchmark-specific loaders, prediction adapters, and evaluator calls | -| `pyproject.toml` | Package metadata and Python dependencies | -| `docs/` | Installation-linked guidance, benchmark research, and contribution notes | -| `chroma_data/` | Local ChromaDB index and `index_status.json` readiness record (generated; do not commit) | -| `benchmark_runs/` | Isolated programmatic and benchmark runs (generated; do not commit) | -| Model caches | Managed by WhisperX, SentenceTransformer, and CLIP outside the repository | +- Open an issue before a large cross-capability or architecture change. +- Keep pull requests focused on one user-visible outcome. +- Preserve the shared application contracts: CLI, UI, HTTP, and MCP should not + grow separate implementations of the same operation. +- State whether a storage/model change requires existing repositories to be + rebuilt. +- GPU support is deferred. Do not silently make CUDA the default or add a new + GPU installation path without the documented validation gates. -The full local CLI/UI index uses up to three collections: -`dialogue`, `scene`, and `actor`. Runs containing -selected capabilities create only the collections they need. +## Development setup -## Setup +### 1. Clone and install the complete contributor environment -Follow the [installation guide](../INSTALLATION_GUIDE.md) or install from the -package metadata directly. +```bash +git clone https://github.com/grayhatdevelopers/vidxp.git +cd vidxp +uv sync --frozen \ + --extra local-worker \ + --extra frontend \ + --extra mcp \ + --extra server \ + --extra test \ + --extra benchmarks +``` + +The repository lock selects CPU PyTorch on Linux and Windows. + +### 2. Initialize the media runtime ```bash -python -m venv venv -# Windows: venv\Scripts\activate -# macOS/Linux: source venv/bin/activate -python -m pip install -e ".[all,frontend,benchmarks]" +uv run --no-sync vidxp init ``` -Verify the environment: +This verifies FFmpeg, ffprobe, `libx264`, and `aac`. It may offer an explicit, +confirmed system package-manager command; Python dependency installation never +changes system packages. + +### 3. Verify the environment ```bash -vidxp --version -vidxp doctor +uv run --no-sync vidxp --version +uv run --no-sync vidxp doctor +uv run --no-sync pytest -q ``` -CLI: `vidxp --help` -UI: `vidxp ui` - -Models default to CPU and download into their libraries' standard caches on -first use. If a model identifier changes, update the setup documentation and -record the exact identifier in benchmark results. - -## Where to put work - -- Capability-specific indexing, retrieval, models, schemas, and dependencies: - the matching folder under `src/vidxp/capabilities/`. -- Shared storage, media handling, run state, and execution mechanics: - `src/vidxp/core/`. -- Transport-neutral application operations: `src/vidxp/application.py`. -- Command-line behavior: `src/vidxp/cli.py`. -- Upload and search UX: `src/vidxp/frontend.py`; keep product logic in the - shared application and core modules. -- Official benchmark formats and evaluator calls: `src/vidxp/benchmarks/`. -- Capability dependencies: that capability's `requirements.txt`; wire a new - install extra into `pyproject.toml`. -- Product direction: the roadmap in the main [README](../README.md). - -Prefer small, focused pull requests. If you change how embeddings or metadata -are stored, state whether an existing `chroma_data` index must be rebuilt. - -## Before you open a PR - -1. Run the automated test suite relevant to the change. -2. Index a short sample video, then try dialogue, scene, and—if touched—actor - search. -3. If you changed the Streamlit app, smoke-test upload, indexing, cancellation, - reload, and search. -4. Confirm that an incomplete local index is replaced rather than treated as - ready. -5. Do not commit model weights, generated indexes, benchmark runs, or local - sample media. - -Run the complete automated suite with: +`doctor` may report that model artifacts have not been prepared; that is +expected in a fresh contributor environment. Dependency, import, FFmpeg, or +codec failures are not expected. Prepare only the modality needed for manual +testing: ```bash -python -m unittest discover -s tests +uv run --no-sync vidxp prepare --modalities scene +uv run --no-sync vidxp doctor --modalities scene ``` -## Pull requests +Models and normal local repositories use the operating system’s per-user VidXP +data directory. They are not written into the checkout unless you explicitly +override `--data-dir`. + +## Project map + +- Product operations and contracts live in `application.py`, + `control_plane.py`, `application_models.py`, and the media, artifact, and + query services. +- Capability-specific indexing and retrieval live in `capabilities/`. +- CLI, Streamlit, HTTP, and MCP adapters live in `cli.py`, `cli_commands/`, + `frontend.py`, `api.py`, `api_routes/`, and `mcp.py`. +- Durable execution and storage live in `job_service.py`, `workflow_*`, + `execution.py`, `core/`, `infrastructure/`, and `ports.py`. +- Deployment and desktop packaging live in `Dockerfile`, `compose*.yaml`, and + `desktop/`. +- Tests are under `tests/`; benchmark adapters and documentation are under + `src/vidxp/benchmarks/` and `docs/benchmarking/`. +- Optional dependency groups are under `src/vidxp/requirements/`. + +Generated environments, model weights, media, indexes, artifacts, +`benchmark_runs/`, build outputs, and local data do not belong in commits. + +## Architecture rules + +### Put logic at the correct boundary + +- Capability-specific models, indexing, retrieval, schemas, and dependencies: + `src/vidxp/capabilities//`. +- Transport-neutral operations and result models: the application/control + plane. +- CLI, Streamlit, HTTP, and MCP: thin input/output adapters. +- Filesystem, Chroma, PostgreSQL, DBOS, FFmpeg, and process supervision: + infrastructure/ports. +- Benchmark-specific data formats and evaluator behavior: benchmarks only. + +### Preserve public contracts + +- Public commands/results use stable IDs and metadata, not storage keys or + local filesystem paths. +- Local and server adapters call the same typed application operations. +- Long-running work crosses the durable job boundary. +- Model downloads happen only through explicit preparation. +- Media and artifact publication remains atomic. +- A failed or cancelled generation must not replace the previous active + snapshot. +- Server control processes remain model-free; provider work belongs in workers. +- Local defaults remain under the per-user VidXP data root, not the current + working directory. + +### Adding or changing a capability + +Follow [Adding a capability](adding-a-capability.md). At minimum: + +- declare the definition and typed operation schemas; +- keep provider dependencies in that capability’s `requirements.txt`; +- pin model identity, revision, checksum, license metadata, and disclosed size; +- wire the package extra in `pyproject.toml`; +- test provider readiness without downloading models; and +- document whether old indexes must be rebuilt. + +## Validation + +Run the smallest relevant checks while iterating, then the complete applicable +gate before opening a pull request. + +| Change | Minimum validation | +|---|---| +| Python logic or contracts | Targeted pytest file + full `pytest -q` | +| CLI | Command help, success path, failure path, JSON output | +| Streamlit | Full-page render, form submit, job progress/terminal state, reload | +| HTTP API | Shared application test + route/auth/error contract | +| MCP | Tool contract + `vidxp-mcp --check` | +| Media/artifacts | Real ffprobe/FFmpeg smoke + path/ID confinement | +| Models/providers | Dependency check, prepared/unprepared states, short real-media smoke | +| Desktop | Rust tests, JavaScript syntax, setup/close lifecycle | +| Docker/Compose | Dockerfile targets, `docker compose config`, health checks | +| Documentation | Commands, relative links, headings, and rendered tables | + +Common commands: -- Clear title and a few bullets on what / why. -- Note any new env vars, model downloads, or breaking index format changes. -- Link a related issue when there is one. +```bash +uv run --no-sync ruff check . +uv run --no-sync pytest -q +node --check desktop/web/app.js +docker compose config --quiet +``` + +Desktop validation requires the pinned uv sidecar before Rust tests: -### Changelog fragments +```bash +npm --prefix desktop install +npm --prefix desktop run sidecar:windows +cargo test --manifest-path desktop/src-tauri/Cargo.toml +``` + +Use `npm --prefix desktop run sidecar:unix` on macOS or Linux. Validate +`compose.coolify.yaml` only with a complete deployment test environment; its +required image, secret, hostname, and upload variables intentionally make a +bare `docker compose config` fail. + +Model-backed manual smoke tests should use a short sample and only the +modalities affected by the change: + +```bash +uv run --no-sync vidxp media import samplevideo.mp4 --json +uv run --no-sync vidxp index create --modality scene +uv run --no-sync vidxp search scene "a person enters the room" +``` + +Do not describe mocked adapter tests as end-to-end validation. If a test does +not exercise a real process, provider, codec, database, browser, or protocol +boundary, say so. + +## Pull requests + +The repository's +[pull request template](../.github/pull_request_template.md) is the required +starting point. GitHub loads it automatically for new pull requests. Complete +all three sections: + +- **Summary:** describe the user-facing outcome and important compatibility or + migration behavior. +- **Validation:** list the exact commands and real boundaries exercised. Do not + replace results with “tests passed.” +- **Changelog:** add the correct fragment, or explain why the change is + dependency-only/internal and should receive `skip-changelog`. + +Keep the description compact, but include: + +- what users can do after the change; +- why the change is needed; +- important design or compatibility decisions; +- new dependencies, environment variables, downloads, or migrations; +- validation actually performed; and +- screenshots or sample output for user-interface changes. + +Use Conventional Commit prefixes for commits, for example: + +```text +feat(mcp): add clip artifact discovery +fix(frontend): keep search form submittable +docs: clarify local installation profiles +``` -Add one `changes/..md` file for every user-visible pull -request. Use one of these types: +## Release notes and versions -- `breaking` for incompatible behavior or API changes. -- `feature` for new behavior. -- `bugfix` for corrected behavior. -- `deprecation` for behavior scheduled for removal. -- `docs` for user-facing documentation improvements. -- `security` for security fixes or hardening users should know about. +Release Please derives versions and changelog entries from Conventional +Commits. Make the squash-merge commit or the commits retained by a regular +merge describe the public change accurately: -The fragment should be one sentence written for users. Do not include a heading, -version number, commit message, or implementation details. See -[`changes/README.md`](../changes/README.md) for an example. +- `feat` creates a feature release. +- `fix` and `perf` create a patch release. +- `!` or a `BREAKING CHANGE:` footer creates a breaking release. +- `docs`, `test`, `ci`, `build`, `refactor`, and `chore` do not publish by + themselves. -Dependency updates carrying GitHub's `dependencies` label do not require a -fragment. For other internal-only maintenance, explain the reason in the pull -request and ask a maintainer to apply `skip-changelog`. CI requires a fragment -unless one of those labels is present. +Write user-visible `feat`, `fix`, and `perf` subjects for users, not for the +implementation history. Internal corrections to an unreleased feature should +remain part of that feature rather than appear as fictional public bug fixes. +Release Please prepares the version and component changelogs in a pull request; +do not edit released changelog sections by hand. -Towncrier renders pending fragments as prerelease notes, then collects them into -`CHANGELOG.md` and removes them when a stable release is made. Do not edit the -changelog or package version in a feature pull request. +## Documentation style -## Questions +- Lead with the outcome, then the command or decision. +- Prefer short sections, bullets, and comparison tables over dense paragraphs. +- Keep the README product-focused and the installation guide procedural. +- Put deployment internals in `docs/deployment/`. +- Mark previews, deferred work, and unsupported topologies explicitly. +- Never publish a command sequence that skips a required install, + initialization, preparation, or activation step. -Follow [Adding a capability](adding-a-capability.md) for the complete extension -contract. Open an issue before large cross-capability refactors so scope stays -aligned with the roadmap. +## Getting help -Maintainers should follow the [release process](releasing.md). +- Open an [issue](https://github.com/grayhatdevelopers/vidxp/issues) for a bug + or scoped proposal. +- Use [Discord](https://grayhat.studio/discord) for contributor discussion. +- Maintainers should follow the [release process](releasing.md). diff --git a/docs/architecture/platform.md b/docs/architecture/platform.md new file mode 100644 index 0000000..41e5c09 --- /dev/null +++ b/docs/architecture/platform.md @@ -0,0 +1,1505 @@ +# VidXP Platform Architecture + +Status: accepted +Last validated: 2026-07-28 +Scope: CLI, API, MCP, Streamlit, desktop, media ingestion, indexing, search, +artifacts, natural-language query, CPU/GPU execution, and deployment + +## 1. Purpose + +This document defines the target architecture and delivery gates for VidXP's +application platform. Changes to its ownership boundaries or accepted decisions +require a reviewed architecture update or ADR with supporting evidence. + +## 2. Product outcomes + +VidXP must support: + +1. A local CLI operating directly on local media and repositories. +2. A local interactive UI, currently Streamlit, using the same application layer. +3. A future packaged desktop UI using the same application layer. +4. A remotely accessible HTTP API. +5. A remotely accessible MCP server over Streamable HTTP. +6. Local MCP over stdio for adjacent clients; stdio is not the media path. +7. Durable indexing and artifact jobs with progress and cancellation. +8. CPU execution first and an explicit, tested GPU runtime afterwards. +9. Natural-language questions over all indexed collectors with timestamped evidence. +10. A Coolify-compatible multi-service deployment. +11. Native local operation on supported Apple Silicon macOS without requiring Docker. +12. A thin-client mode for using a self-hosted VidXP server from another machine. + +## 3. Non-negotiable invariants + +1. CLI, UI, API, MCP, and desktop are adapters. They parse input, authenticate, + invoke application commands, and format output. They do not implement indexing, + media persistence, job state, search policy, or artifact generation. +2. FastAPI and MCP are sibling adapters. MCP never calls VidXP's HTTP API internally. +3. Public transports reuse the same Pydantic command and result models. +4. Video bytes are never passed as MCP tool arguments or over stdio. +5. Remote MCP clients reference previously registered media by `media_id`. +6. A failed or cancelled indexing job cannot damage the last committed index. +7. Search reads an immutable committed index generation. +8. Job state and index state are separate. +9. No remote contract exposes arbitrary server filesystem paths. +10. CPU/GPU selection is a validated runtime profile, not a free-form string. +11. Unexpected internal errors are logged with correlation data and masked publicly. +12. Every unbounded collection exposed by an adapter is paginated or capped centrally. +13. Stable dependencies are kept current; compatibility pins require a recorded + blocker, an owner, and a removal gate. +14. CPU behavior across Linux, Windows, and Apple Silicon is accepted before + platform-specific acceleration work. + +## 4. Dependency direction + +```text +Composition root + ├── Typer CLI adapter + ├── Streamlit adapter + ├── Desktop adapter + ├── FastAPI adapter + └── MCP adapter + │ + ▼ +Application commands, queries, results, and public errors + ├── media use cases + ├── indexing use cases + ├── search use cases + ├── natural-language query use cases + ├── actor use cases + └── artifact use cases + │ + ▼ +Domain contracts and ports + ├── MediaCatalog / MediaStore + ├── IndexCatalog / IndexRepository + ├── JobService + ├── ArtifactStore + ├── CapabilityRegistry + ├── QueryPlanner / AnswerSynthesizer + ├── ModelRuntime + └── ResourceScheduler + │ + ▼ +Infrastructure implementations + ├── managed local filesystem storage + ├── Chroma + ├── DBOS SQLite / Postgres + ├── FFmpeg / ffprobe / OpenCV + ├── local and GPU model providers + └── environment and Docker wiring +``` + +Only the composition root may import both adapters and concrete infrastructure. +Application and domain modules must not import FastAPI, MCP, Typer, Streamlit, +Chroma, DBOS, OpenCV, or environment variables. + +## 5. Composition and settings + +Use one immutable `VidXPSettings` model built with `pydantic-settings`. + +It owns: + +- the per-user application data root +- repository root and active local repository selection +- trusted local import roots +- upload limits and retention +- authentication mode and secrets +- public base URL +- MCP transport configuration +- MCP canonical resource URL, body limit, allowed hosts, and allowed origins +- local/server workflow mode and queue selection +- resolved CPU/GPU runtime request +- model cache path and offline/download policy +- allowed model identifiers +- concurrency and memory-related limits + +Settings are constructed once by a composition root and injected. Adapters must not +mutate process environment variables to communicate configuration. Application +objects must not be created at module import time. + +### 5.1 Version and upgrade policy + +VidXP targets the latest stable, non-pre-release versions available when a delivery +phase begins. As of this validation, the current Python release is 3.14.6. Runtime +images and primary development use the latest 3.14 maintenance release. +The library test matrix covers supported CPython 3.11 through 3.14 while those +versions remain security-supported. + +Direct dependency ranges are bounded at the next incompatible major version. +Release environments and images lock exact direct and transitive artifacts. Update +automation opens dependency changes continuously, but an update merges only after +contract, packaging, platform, benchmark, and license gates pass. + +A dependency may remain below latest stable only when a reproduced incompatibility +or material regression is recorded. The exception must identify: + +- the blocking package and upstream issue +- affected operating systems/runtime profiles +- the newest compatible version +- the replacement or upgrade path +- the test that removes the exception + +An ML package does not hold the control plane, Python runtime, or unrelated +capabilities back. When necessary, the package is replaced or isolated behind its +worker/provider contract. Pre-release packages are excluded from production +constraints. + +Model weights follow a different rule from libraries: the default is the +best-performing current stable model that passes VidXP's quality, latency, memory, +license, redistribution, and cross-platform benchmarks. It is pinned by immutable +revision and checksum. “Newest” alone is not sufficient to change indexed embedding +semantics. + +## 6. Shared public contracts + +The application layer owns the canonical Pydantic models. HTTP and MCP may project +transport-only fields such as links or status codes, but may not redefine business +state. + +Required identifiers: + +- `RepositoryId` +- `MediaId` +- `VideoId` +- `IndexGenerationId` +- `IndexSnapshotId` +- `JobId` +- `ArtifactId` + +For the current single-source media pipeline, `VideoId` is the same opaque value +as `MediaId`. A separate video-track identity is introduced only when one media +asset can produce multiple independently addressable video tracks; adapters must +not invent a second identifier before then. + +Required shared models: + +- `MediaAsset` +- `MediaProbe` +- `IndexPlan` +- `IndexGeneration` +- `IndexSnapshot` +- `IndexGenerationStatus` +- `Job` +- `JobProgress` +- `ProgressEvent` +- `SearchCommand` +- `SearchHit` +- `SearchResult` +- `QueryPlan` +- `QueryAnswer` +- `Evidence` +- `Artifact` +- `Page[T]` +- `RuntimeProfile` + +Public models carry a real schema version where compatibility requires one. Adapters +must not append schema fields manually during serialization. + +## 7. Public error model + +Application errors have: + +- stable machine code +- category +- safe public message +- optional safe details +- retryability +- internal cause retained only for logs + +Initial categories: + +- validation +- authentication +- authorization +- not found +- conflict +- unavailable +- resource limit +- cancelled +- internal + +FastAPI maps categories to HTTP statuses and a consistent error envelope. MCP maps +expected application failures to controlled tool-execution errors. CLI maps them +to exit codes and terminal messages. No adapter infers product meaning from broad +built-in exceptions. + +Every MCP tool is registered through one wrapper that catches expected application +errors and raises the SDK's public `ToolError` with a compact safe JSON payload: + +- an assigned safe protocol integer code +- a safe public message +- the stable VidXP application code, retryability, and correlation ID +- safe validation details or the required repository scope when applicable + +The wrapper catches every other exception, logs the cause with correlation data, and +raises only a generic internal `MCPError`. `MCPServer` re-raises `MCPError` without +converting the underlying exception to text, so the SDK's default +`str(exception)` leakage path is not used. Expected errors become +`CallToolResult(isError=True)` through the SDK's normal execution-error path and +remain outside the Pydantic success schema. + +## 8. Repository layout + +Native local interfaces share an operating-system per-user application data +root rather than deriving storage from the process working directory: + +```text +VidXP/ + repositories/ + default/ + ... + models/ +``` + +The CLI's global `--data-dir` option and `VIDXP_DATA_DIR` replace this root. +`VIDXP_INDEX_DIR` and `VIDXP_MODEL_CACHE` remain narrower advanced overrides. +Named-repository configuration uses the operating system's standard roaming +user-configuration directory. Container deployments do not inherit these host +defaults; Compose supplies explicit repository and model-cache volumes. + +Within a repository, one `RepositoryLayout` defines all persistent paths: + +```text +repository/ + repository.json + catalog.sqlite3 + media/ + objects/ + indexes/ + store/ + generations/ + / + snapshots/ + .json + active-snapshot.json + artifacts/ + objects/ + local-workflows/ +``` + +Model caches do not live inside repositories. For native local operation they +are a sibling under the shared application data root; server deployments mount +their cache explicitly. + +Remote API/MCP responses never expose these internal paths. + +## 9. Media ingestion and identity + +### 9.1 Media asset + +`MediaAsset` records: + +- stable `media_id` +- content checksum +- original filename +- byte size +- declared and detected MIME/container +- duration, streams and codecs from ffprobe +- managed storage key or approved external source reference +- repository and owner/principal where applicable +- ingest state +- associated `video_id` +- creation and retention timestamps + +Checksum is calculated once during ingest and reused for deduplication and indexing. +Untrusted content is not published into the catalog until ffprobe validation succeeds. + +### 9.2 Local CLI and desktop + +Local clients call `ImportMedia` with a path. Policy decides whether VidXP references +the original file or copies it into managed storage. Paths must resolve beneath +configured import roots when a boundary is required. + +Local media does not need to travel through an HTTP upload endpoint. + +### 9.3 Remote upload + +Remote upload is a four-stage protocol: + +1. An authenticated client creates an upload intent. +2. tusd's blocking `pre-create` HTTP hook authenticates the request, validates the + declared size/type and intent, and assigns an opaque upload ID. +3. tusd returns an HTTPS upload URL. That URL is an unscoped bearer credential for + subsequent HEAD/PATCH requests because tusd cannot bind resumptions to the user + who created the upload. +4. The non-blocking `post-finish` hook idempotently upserts completion by upload ID + and enqueues the durable ffprobe/import workflow. + +Application responses carrying upload URLs use `private, no-store` and +`no-referrer`; hook payloads and MCP results do not persist them. The opaque path is +still a bearer credential and may appear in an upstream proxy's access log unless +that deployment disables or redacts logging for `/uploads/`. Hook handlers assume +duplicate and out-of-order delivery and never run ffprobe or encoding inline. A +recovery sweep finds completed uploads whose finish hook was missed. A retention +workflow removes abandoned intents and quarantine objects. + +The hook endpoint is private to the Compose network. Client authorization is read +from the hook request body and redacted; client tokens are never stored in tus +metadata. Only the tus upload route is public. A completed upload is not a +`MediaAsset` until durable probe/import succeeds. + +The supported server topology uses tusd filestore on a named quarantine volume +shared read-only with the hook service and worker. Managed media and artifacts use +the stack's named content volume. The whole deployment is single-node and one +deployed stack is one repository boundary. Clients upload directly to tusd; +FastAPI does not proxy large video bodies. + +S3-compatible storage is deferred and not implemented. If revisited, it would +apply only to upload quarantine, source media, and generated artifacts—never to +embeddings, which remain in Chroma. Although tusd can receive uploads into S3, +VidXP would still require explicit import, processing, publication, recovery, +cleanup, and delivery integration. + +tusd 2.10.0 is pinned to a tested release digest, uses an explicit base path/public HTTPS +URL, a default 50 GiB maximum upload size, a deployment-wide upload quota, +restricted CORS origins, disabled downloads, and disabled concatenation. Deployments +may lower the configured maximum. Termination remains enabled so tusd owns its file +locks and deletion. +The blocking `pre-terminate` hook prevents deletion while import is processing and +admits completed/expired cleanup only with the private cleanup credential. + +FastAPI retains one small multipart compatibility endpoint with a central hard +maximum of 256 MiB. It rejects oversized declared `Content-Length` before reading +and enforces the same maximum while streaming to quarantine. It is never used for +multi-gigabyte media. + +URL ingestion is deferred. If added later, it is an asynchronous downloader with +SSRF controls, redirect and DNS revalidation, size/time limits, quarantine, and +ffprobe validation. + +### 9.4 MCP and media + +Remote MCP workflow: + +```text +HTTP upload/import service -> media_id -> MCP start_indexing(media_id) +``` + +MCP does not carry video bytes, base64 video, or arbitrary server paths. Stdio may +operate on an existing local `media_id`, but is not a video injection protocol. + +## 10. Index generations and repository snapshots + +An `IndexGeneration` is an immutable index of one media asset under one capability +and model/runtime manifest. An `IndexSnapshot` is an immutable repository view that +maps each active `media_id` to the generation containing its current searchable +records. + +This separation supports incremental libraries. Adding or re-indexing one video does +not rebuild every other video and does not mutate the currently searchable version. + +An upsert indexing job: + +1. Resolve and validate the media asset. +2. Acquire a lease for that repository and media asset. +3. Build under `indexes/generations/`. +4. Persist manifests and checkpoints inside the generation. +5. Validate capability completion and storage consistency. +6. Create a new snapshot by replacing only that media asset's generation mapping. +7. Atomically update `active-snapshot.json`. +8. Release the lease. +9. Retain or garbage-collect unreferenced generations according to policy. + +Removing media creates a snapshot without that media mapping; it does not delete +index data before the new snapshot is active. A full rebuild creates generations for +the selected assets and commits one new snapshot after all required generations +succeed. + +Failure or cancellation before step 7 leaves the prior snapshot and prior generation +searchable. + +Search resolves `active-snapshot.json` once and queries only generations referenced +by that immutable snapshot. Indexing and search therefore coexist safely. + +The Chroma adapter stores generation identity with every record and implements +snapshot-scoped search and garbage collection. Chroma remains replaceable behind the +`IndexRepository` port; snapshot semantics do not depend on Chroma collection layout. +For the embedded adapter, `indexes/store/` is the shared physical Chroma database; +generation directories own manifests and checkpoints, while exact generation record +counts in those manifests are revalidated before committed reads. A missing database, +collection, manifest, or referenced record therefore fails closed instead of creating +an empty replacement during a read. + +The manifest is authoritative for completed generation state. Progress files and +workflow events are not used to decide whether a generation or snapshot is valid. + +## 11. Durable jobs and process execution + +The application exposes one `JobService` contract: + +- submit +- get +- list +- cancel +- retry/resume where safe +- publish progress +- retrieve result + +DBOS 2.28 is the durable orchestration, queue, and state backend: + +- SQLite for local CLI/UI/desktop +- one Postgres database for the API catalog, uploads, snapshots, and DBOS's + isolated `dbos` schema in API/Coolify +- the same workflow definitions and job contract in both modes +- separate CPU and GPU worker queues +- concurrency one by default for indexing per constrained runtime + +DBOS owns durable lifecycle, queueing, recovery, status, attempts, and persisted +progress. `set_event` stores the latest `JobProgress`; append-only DBOS streams are +used only when event history is required. Workflow stream writes are exactly once; +step stream writes may repeat after retry. The index manifest remains authoritative +for index state. + +DBOS synchronous workflows and steps run in threads inside the worker process. +DBOS does not provide process isolation or pre-empt blocking CPU/GPU calls. Heavy +work therefore always runs in a dedicated worker process/container, never in the +CLI, desktop, UI, API, or MCP process: + +```text +local: +CLI/UI/desktop -> DBOSClient -> supervised local worker process -> SQLite + +server: +API/MCP -> DBOSClient -> worker process/container -> Postgres +``` + +The local client may detach from the durable worker. SQLite is restricted to one +machine and local storage; it is not used on network filesystems or across hosts. + +The core runner accepts one execution context with progress and cooperative +cancellation. Queued cancellation removes the DBOS workflow. Cancelling an active +synchronous DBOS step only marks it cancelled and is observed at a later step; it +does not stop the running thread. VidXP therefore bridges durable cancellation to +the runner's `CancellationToken` and polls at progress/batch boundaries. Blocking +model and FFmpeg calls that expose no cancellation point finish before cancellation +is observed. Hard interruption is an operator-level termination of the dedicated +concurrency-one worker followed by supervisor restart; it is not promised as a +safe per-call API feature. + +This cooperative/process boundary is execution control, not another job-state +machine. VidXP does not recreate queue/job lifecycle in JSON and locks. + +Retry is enabled only for idempotent steps. Generation-based indexing makes retries +safe because incomplete generations are never active. + +Local workers use a stable executor ID. Server executor IDs are +`--` and remain stable across replacement of the +same release, while blue/green releases receive distinct IDs. The v1 server topology +uses exactly one CPU executor plus exactly one GPU executor when the GPU profile is +enabled. Each calls `DBOS.listen_queues()` before launch. The API uses `DBOSClient` +and never launches or listens to queues. A same-release replacement recovers work +owned by its stable ID; an old-version worker remains available while its workflows +drain. Scaling a release to multiple ordinals requires DBOS Conductor or a separately +reviewed dead-executor recovery coordinator. + +`worker_concurrency=1` limits one executor; repository leases enforce per-repository +exclusion. Queue-global concurrency is configured only where one job across every +worker is intentionally required. + +`application_version` combines the VidXP release with a digest of the installed +workflow implementation. Recovery therefore occurs only on a worker running the +same code, including when a prerelease build changes without a package-version +bump. Deployments drain old-version jobs or run blue/green old-version workers until +they finish; breaking workflow changes require an explicit DBOS patch/migration +strategy. + +## 12. Resource scheduling and model runtime + +`RuntimeProfile` is resolved at startup: + +- operating system and architecture +- requested backend: `auto`, `cpu`, `cuda:`, or `mps` +- resolved backend per capability +- precision/compute type per capability +- model cache +- offline/download policy +- maximum concurrent indexing jobs +- maximum concurrent inference queries +- allowed model IDs and revisions + +Hardware probing occurs once. Invalid CUDA or MPS configuration fails readiness +clearly instead of falling back silently. + +`auto` is allowed only for local CLI/desktop. It resolves once, records the actual +backend used by each capability in the job/index manifest, and never changes during +a job. A capability may resolve differently from another capability on the same +machine; for example, scene embedding can use Apple MPS while transcription uses +CPU. Server/Coolify workers require explicit `cpu` or `cuda:` so scheduling +cannot drift. + +Model construction and caching belong to `ModelRuntime`. Adapters and capability +handlers do not each load models independently. + +Model downloads are explicit preparation jobs. Doctor/readiness checks inspect +the pinned cache without constructing models or downloading, and ordinary +indexing or query work fails fast with `model_unavailable` when required +artifacts are absent. Preparation publishes byte progress and model-loading +stages through the shared durable job contract. Pinned snapshot and artifact +byte sizes are part of the model contract. Interactive preparation displays +the missing-model sizes, maximum additional cache space, and cache path before +requiring confirmation. + +The scheduler bounds concurrent model work. Local execution does not reject work +based on a fixed free-RAM threshold; allocation failures come from the runtime +that attempted the actual operation. API request threadpools are not the model +scheduler. + +### 12.1 Scene sampling delivery boundary + +The current scene-indexing deliverable uses deterministic time-based sampling at +a configured interval. This is the minimal sampling path required to make scene +indexing predictable and runnable through the complete application; it does not +claim content-aware or shot-aware frame selection. + +Content-aware or shot-boundary sampling is deferred. Before it is considered for +the default path, benchmark it against time-based sampling for retrieval quality, +indexing time, materialized frames, and memory use. Add sampling-specific +backpressure only if those measurements show that bounded model/write batches and +the existing scheduler do not adequately control resource use. + +## 13. Cross-platform CPU and acceleration + +CPU completion is the first platform gate. The supported local targets are: + +- Apple Silicon macOS on the current and previous two major macOS releases +- Linux x86-64 +- Windows x86-64 + +Linux ARM64 is accepted only after the same native-wheel and model smoke gates pass. +Intel macOS is not a target for the new local runtime. + +The native macOS path does not require Docker. It uses: + +- the current stable CPython ARM64 installer/runtime +- a supervised local worker and DBOS SQLite +- embedded Chroma +- local FFmpeg/ffprobe +- PyTorch MPS for capabilities that pass parity tests +- CPU inference where a provider has no stable Metal backend +- native Ollama, which manages its own Metal acceleration + +The initial macOS transcription provider is CPU-capable. An MLX provider can replace +or supplement it only after it passes the same transcript/timestamp contracts; the +rest of VidXP does not depend on that optimization. + +CUDA is a later delivery phase, after the CPU application, API/MCP, media, jobs, +index snapshots, and deployment paths are complete. It uses the same application +contracts with a distinct Linux worker image: + +- the latest mutually compatible stable Python, PyTorch, CTranslate2, CUDA, and + cuDNN versions at the start of the GPU phase +- Docker/Compose GPU device reservation +- explicit precision and batch defaults per capability +- worker readiness that verifies CUDA without downloading models + +The GPU phase does not inherit the old PyTorch 2.8/WhisperX matrix. If a package +requires an obsolete Python/PyTorch/CUDA stack, replace or isolate that package +instead of downgrading the platform by default. `torchaudio` and `torchvision` are +installed only when a selected provider directly requires them. + +PyTorch CPU/CUDA variants are selected through separate locked constraints and +explicit package indexes. An `extra-index-url` is not used where it can mix CPU and +CUDA artifacts. + +Capability routing is explicit after acceleration acceptance: + +- scene and text embeddings: CPU everywhere; MPS on supported Macs and CUDA on the + GPU worker after parity tests +- transcription: CPU everywhere; CUDA through the selected CTranslate2 provider + after parity tests; an Apple MLX provider is a separate optimization +- actor: CPU until its ONNX GPU provider passes clustering/search parity +- SLM/Ollama: CPU/Metal locally; CPU by default in the server profile + +API/MCP never receives CUDA access. When the GPU profile is enabled, only +`worker-gpu` receives CUDA access. An optional Ollama CUDA profile can receive a +different explicit `device_id`. Sharing one device between the indexing worker and +Ollama requires a later cross-process VRAM admission design. + +Readiness reports platform, architecture, provider versions, selected backend, +precision, and device details. CUDA readiness reports driver, compiled CUDA, +device index/capability, and `torch.cuda.is_available()`. MPS readiness reports +`torch.backends.mps.is_available()`. Unsupported requested acceleration fails +instead of silently using CPU. + +Model weights are not baked into the default release image. First-use download and +offline/preload behavior are documented and observable. Preloaded model images are +not part of this architecture. + +## 14. Capability system + +Capability definitions contain domain metadata only: + +- name and description +- supported index stages +- operations and their shared input/result models +- execution group +- required media/index data +- model/runtime requirements +- query affordances and expected cost + +They do not contain Typer factories or MCP/FastAPI metadata. + +Built-ins are registered by the composition root. External capabilities use Python +package entry points. Execution grouping is explicit; it is not inferred from Python +callable identity. + +Capabilities depend on storage/model/media ports. Chroma and model packages are +infrastructure implementations. + +External packages register under entry-point group `vidxp.capabilities`. Each entry +point loads a zero-argument factory returning a transport-neutral +`CapabilityPlugin` containing a `CapabilityDefinition`, application executor +factory, and declared VidXP capability-contract version. The definition remains +pure metadata; the executor factory binds operations to handlers that depend only on +application/domain ports. + +External capabilities are trusted in-process code, not sandboxed plugins. They are +disabled by default in remote deployments and enabled by an allowlist of distribution +and entry-point name. Built-in names cannot be overridden. Duplicate names, +unsupported contract versions, import failures, and missing runtime requirements +fail readiness with distribution provenance. Discovery/loading occurs only in the +composition root and is sorted deterministically. + +### 14.1 Collector dependency decisions + +- Scene: remove `clip-anytorch` and its `setuptools<81` compatibility pin. Use + Transformers with `google/siglip2-base-patch16-224` at immutable revision + `75de2d55ec2d0b4efc50b3e9ad70dba96a7b2fa2`. The 2025 SigLIP 2 paper reports + retrieval improvements over SigLIP across model scales, and MIEB independently + places SigLIP-family models among strong multimodal retrieval encoders. That + evidence is recent and matches the image/text retrieval task closely enough that + downloading several large candidates for another selection benchmark is not + justified in this delivery phase. VidXP's existing scene benchmark remains the + regression gate. +- Dialogue: replace WhisperX as the default provider because its stable release + blocks current Python and constrains an older Torch family. The first replacement + is latest stable `faster-whisper`/CTranslate2 using batched transcription, VAD, and + word timestamps. The default is + `dropbox-dash/faster-whisper-large-v3-turbo` at immutable revision + `0a363e9161cbc7ed1431c9597a8ceaf0c4f78fcf`. Its transcript/timestamp output is + tested against the existing + dialogue contract. Forced alignment is an optional provider behind a separate + contract; it cannot hold base transcription or Python back. Sentence embeddings + use Qwen3-Embedding-0.6B at immutable revision + `97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3`; its published multilingual MTEB + retrieval results materially exceed the older multilingual E5 baseline and its + Apache-2.0 license permits the intended deployment. +- Actor: replace `face_recognition`/dlib with OpenCV Zoo YuNet plus SFace through + OpenCV's maintained DNN APIs. Model files are retrieved with `pooch`, pinned to + OpenCV Zoo commit `47534e27c9851bb1128ccc0102f1145e27f23f98`, and verified + against recorded SHA-256 digests before use. + Migration requires clustering/search parity, a new index-schema/model identity, + and CPU performance gates. Actor remains CPU-routed until the ONNX GPU provider + passes the same gates. +- Storage: embedded Chroma `PersistentClient` is local CLI/desktop only. Server + deployments use the latest stable Chroma client/server release behind the + `IndexRepository` port. + +`ModelRuntime` records provider, canonical model ID, immutable revision/digest, +weight checksum, license identifier, precision/quantization, cache state, and offline +policy in every generation manifest. Transitive VAD/alignment/model downloads are +rejected unless allowlisted. + +## 15. Search + +`SearchMoments` is a central application query: + +- query text +- optional modalities/capabilities +- filters from an allowlisted typed grammar +- page size/cursor +- optional reranking profile + +Results are normalized into `SearchHit`: + +- `media_id` +- `video_id` +- source/index generation +- modality +- start/end time +- score and score semantics +- displayable text/metadata +- optional preview reference + +The application owns limits, score normalization, multimodal fusion and deterministic +ordering. UI/API/MCP may request stricter limits but cannot loosen policy. + +Actor cluster and detection queries use the same pagination conventions. + +## 16. Natural-language query layer + +`QueryVideo` is an application use case, not an MCP agent loop and not direct Chroma +access. + +Flow: + +1. Accept question, media/repository scope, and policy. +2. Ask an injected local SLM planner for a strictly typed `QueryPlan`. +3. Validate that the plan uses registered operations and safe parameters only. +4. Execute retrieval through application search/capability operations. +5. Fuse overlapping intervals with deterministic reciprocal-rank fusion. +6. Ask the answer synthesizer for a grounded answer. +7. Return `QueryAnswer` with timestamped citations and supporting hits. + +Pydantic AI is the selected typed-call layer. The first local provider is self-hosted +Ollama 0.32.5, pinned to a tested multi-architecture image digest, through +`pydantic-ai-slim[openai]` and `OllamaModel`. Ollama Cloud is excluded because it +does not enforce the local JSON-Schema path required here. `QueryPlan` and the +model-only `DraftAnswer` use `NativeOutput`, strict Pydantic models, a bounded +request/output retry budget, and explicit time/token limits. VidXP validates the +draft and constructs `QueryAnswer`; the model receives no executable tools. +Pydantic AI does not replace VidXP's capability registry or search storage. + +When the SLM is unavailable or produces an invalid plan, a deterministic fallback +searches applicable modalities and returns evidence. Generated text can never become +a filesystem path, model identifier, arbitrary filter operator, or executable +operation. + +Before returning a generated answer, the application verifies every citation against +the retrieved hit ID, media ID, generation, and time interval. Missing, invented, or +out-of-range citations invalidate the generated answer and trigger the deterministic +evidence fallback. Typed JSON alone is never treated as proof of grounding. + +The result records the configured provider/model identity and plan for +reproducibility. + +The default SLM model ID, immutable revision/digest, and quantization are +intentionally unset until candidate models pass: + +- both output schemas +- adversarial-plan rejection +- grounded-citation verification +- CPU RAM and latency limits +- GPU VRAM limits where applicable +- offline-cache behavior +- license and redistribution review + +Until that gate passes, the provider is fixed to self-hosted Ollama but no arbitrary +caller-provided model string is accepted. Ollama runs as an internal optional `slm` +Compose profile with a persistent model cache; deterministic evidence retrieval +remains available when it is disabled. + +## 17. Artifact and snippet delivery + +`Artifact` records: + +- `artifact_id` +- source `media_id` +- owning job +- type/profile +- MIME and byte size +- checksum/storage key +- state +- creation/expiry + +Media and artifacts are served from the managed local content volume through +protected Starlette `FileResponse`, including byte-range support. + +Every delivery request is handled only after repository authorization. Artifact +delivery resolves an `ArtifactStore` key beneath its configured root, rejects +symlink/path escapes, and only then constructs `FileResponse`. + +Local source playback applies the identical configured-root and symlink/path-escape +checks to the `MediaStore` key before constructing `FileResponse`. + +Search results normally use an authorized range-enabled source URL and client-side +seeking. VidXP does not generate a clip for every search hit. + +`CreateSnippet` is an asynchronous artifact job used for download/share/MCP resource +links. FFmpeg output is cached by source checksum, interval and encoding profile, +written to a temporary key, validated, and atomically published. + +Actor overlays and other rendered media use the same artifact workflow. Public +rendering commands never accept output paths. The separate CLI artifact-download +command may copy an already-authorized managed artifact to an explicit user +destination; API downloads remain protected responses and MCP returns a lazy +resource link instead of embedding video bytes in the tool result. + +## 18. FastAPI adapter + +FastAPI owns: + +- HTTP routing and content negotiation +- authentication dependency and principal projection +- request/correlation IDs +- transport-level body rejection +- status codes and headers +- OpenAPI +- small multipart compatibility +- protected local artifact responses + +FastAPI calls application commands directly. It has no job manager, media store, +index lock, model cache, or MCP client. + +Use shared application models directly where the HTTP shape matches. Add transport +models only for HTTP-specific links or envelopes. + +Provide: + +- `/health` for minimal process liveness +- `/ready` for a minimal aggregate readiness result +- an authenticated runtime-readiness resource for component and device details + +Readiness does not download model weights. + +## 19. Authentication profiles + +### 19.1 Local + +- CLI/UI/desktop in-process: no network authentication. +- Local HTTP bound to loopback: optional static bearer. +- Local stdio MCP: operating-system process boundary. + +### 19.2 Private self-hosted + +A shared static bearer token is permitted as an explicitly documented single-tenant +mode. It uses constant-time verification and produces a typed principal. It is not +described as OAuth and does not imply users, scopes, or revocation. + +For HTTP API and MCP in this mode, one outer ASGI authentication middleware validates +the token before dispatch. The only public-path exceptions are the minimal +`/health` and `/ready` endpoints, which disclose no configuration or component +details. It does not publish fake OAuth issuer/resource metadata. + +The private tusd hook callback is also excluded from public bearer middleware, but +is not a public exception: the reverse proxy does not route it from the public +listener and only the isolated Compose hook network can reach it. The hook handler +validates the original client authorization carried in the `pre-create` hook event +against the upload intent. Later hook events are authorized by the opaque upload ID +and previously authenticated intent, not by assuming that the client bearer will be +present on every resumed request. Hook contents and credentials are redacted. + +### 19.3 Remote/multi-user + +An external OIDC provider issues access tokens. VidXP validates issuer, audience, +expiry and scopes and produces the same typed principal used by API and MCP. + +The MCP server implements OAuth resource-server and protected-resource metadata +required by the protocol. Tokens received by MCP are not forwarded to an internal +HTTP API. + +VidXP's MCP `TokenVerifier`, not the SDK metadata configuration, validates: + +- allowed signature algorithm and signature through cached JWKS +- exact issuer +- `aud` or RFC 8707 resource equal to the canonical public MCP URL +- expiration and not-before +- required scopes + +Only after those checks does it construct the SDK access token and shared VidXP +principal from subject/client ID, scopes, and allowlisted claims. + +Reverse-proxy authentication may be an additional gate but does not replace MCP +resource-server behavior. + +## 20. MCP adapter + +Use the official MCP Python SDK v2 high-level server. + +Primary remote transport: + +- Streamable HTTP +- explicit `stateless_http=True` +- `json_response=False` so disconnect cancellation uses the streaming/SSE path +- application state persisted outside MCP sessions +- explicit lifecycle through the application composition root +- an initial independent MCP request-body limit of 4 MiB + +Local transport: + +- stdio for clients running beside VidXP +- identical tools and shared contracts +- media referenced by `media_id` + +Initial curated tools: + +- `list_capabilities` +- `get_capability` +- `list_media` +- `get_media` +- `get_index_status` +- `start_indexing` +- `search_moments` +- `query_video` +- `list_jobs` +- `get_job` +- `retry_job` +- `cancel_job` + +Media and job discovery let an agent recover registered assets and durable work +without carrying IDs across sessions. Generic job polling, retry, and cancellation +cover indexing, search, and query without duplicating operation contracts. Video +bytes remain on the HTTP/tus ingestion boundary rather than crossing MCP. + +Tool results use real output schemas and structured content. Descriptions remain +short and agent-oriented; full API response schemas are not embedded as prose. + +Every public tool has an explicit Pydantic result annotation and leaves structured +output enabled. Public tools do not return bare primitives/lists because the SDK +wraps them in a synthetic `result` object. Contract tests cover runtime-only Pydantic +validators in addition to emitted JSON Schema. + +Protocol request cancellation is propagated to in-flight search/query work where +safe. It does not silently cancel an already accepted durable indexing job. +Cancellation is cooperative for synchronous CPU/GPU work; blocking functions are +never treated as pre-emptible merely because AnyIO runs them in a thread. + +MCP does not implement its own queue. Persisted VidXP jobs remain the authoritative +long-running operation contract. + +### 20.1 ASGI mounting and lifecycle + +The remote process exposes API and MCP on one public origin while keeping them as +sibling adapters. + +The MCP ASGI app is created with: + +- Streamable HTTP path `/mcp` +- stateless HTTP enabled +- JSON-only responses disabled +- configured MCP body limit +- explicit transport-security settings + +It is mounted at the outer application's root after concrete FastAPI routes. This +keeps both `/mcp` and the RFC 9728 host-root +`/.well-known/oauth-protected-resource/mcp` route reachable. + +The composition-root lifespan enters `mcp.session_manager.run()` exactly once and +orders shared application startup, MCP startup, MCP shutdown, and application +shutdown explicitly. A mounted Starlette child lifespan is not relied on, and MCP +does not start a second copy of shared database/storage/model resources. + +The public/proxy Host and Origin policy is configured explicitly. Binding to +`0.0.0.0` must not disable DNS-rebinding protections. Deployment validation confirms +that the Coolify proxy preserves `Accept`, `MCP-Protocol-Version`, `Mcp-Method`, and +`Mcp-Name`, and does not buffer MCP streaming responses. + +## 21. Streamlit and desktop adapters + +Streamlit retains: + +- widgets and page/session state +- display formatting +- local user interaction + +It loses: + +- fixed global media/artifact paths +- direct file persistence +- index readiness policy +- process and cancellation ownership +- hardcoded capability branching +- model/storage access + +The desktop application uses the same application commands, per-user data-root +layout, and a local DBOS SQLite workflow store, but heavy jobs run in a +supervised separate worker process. It manages client/worker lifecycle, +displays model preparation progress, and detaches from durable work or requests +cooperative cancellation during shutdown. Closing the UI never waits on an +uncancellable model thread inside the UI process. + +The Phase 11 adapter is a small Tauri v2 shell. Its first-run configuration +selects capability extras, optional interfaces, model preparation, and model +storage, while the processing application is the exact published VidXP package +installed into a versioned uv-managed environment. When selected, the Streamlit +adapter is the local human interface on a random loopback port; remote loopback +content receives no Tauri IPC access. Runtime activation is atomic and a failed +configuration retains the prior environment. Tauri owns the Streamlit process +and asks the repository-scoped worker to drain and shut down on exit. + +The first desktop release is an online bootstrap with a native tray supervisor +and no updater. It targets Windows x86-64 NSIS, Apple Silicon DMG, and Linux +x86-64 AppImage. +Target-specific uv binaries are release inputs verified against the pinned +upstream checksum. FFmpeg/ffprobe remain validated system dependencies until +target build provenance, codec selection, and redistribution licenses are +recorded; the application does not silently fetch an unaudited build. + +### 21.1 Operating modes + +The same command and result contracts support three composition profiles: + +1. **Native local:** the CLI, Streamlit, or desktop adapter composes the application + with a local worker, DBOS SQLite, embedded Chroma, and a platform app-data + repository. Media can be imported from allowlisted local paths. This is the + default Apple Silicon experience and does not require Docker. +2. **Remote client (planned):** a thin CLI, UI, or desktop adapter will connect + to a self-hosted VidXP deployment without local models or a vector database. + The current release rejects `VIDXP_MODE=remote` instead of silently composing + local storage. Remote agents are supported now through the Streamable HTTP MCP + endpoint, and other clients can use the authenticated HTTP/tus APIs directly. +3. **Self-hosted server:** API/MCP, workers, workflow state, media ingestion, and + vector search run as the Coolify stack. The same server also supports ordinary + Compose outside Coolify. + +The planned client configuration is limited to server base URL, authentication, +repository, timeouts, and local upload source. Its transport must implement the +same client-facing command interface as the native application facade without +duplicating validation, indexing, search, or artifact policy. + +The distributable Python package has a lightweight client/core installation and +explicit local/server extras. Installing the thin client must not install PyTorch, +model providers, Chroma server components, or CUDA packages. Platform lock files +resolve the local worker dependencies independently from the control plane and thin +client. + +## 22. Deployment topology + +Supported Coolify/Compose server stack: + +```text +api-mcp + ├── FastAPI routes + └── MCP Streamable HTTP mount + +hooks + └── private tusd callback and recovery sweep + +worker-cpu + └── DBOS CPU queue, stable executor ID, concurrency 1 + +postgres + ├── application catalog, uploads, and snapshot metadata + └── DBOS durable workflow state in the `dbos` schema + +chroma + └── private vector-search server with its own volume + +tusd + └── public /uploads/ route and HTTP hook caller + +single-node media/artifact/quarantine volumes +``` + +This is one node, one application stack, and one repository. PostgreSQL, Chroma, +tusd, and the named content volumes are deployment components of that stack, not +provider plug-in points. A separate repository requires a separate stack and +separate databases and volumes. Arbitrary hosted databases, externally shared +Chroma, multiple API/worker replicas, failover, and provider compatibility are +outside the supported topology. + +Server code connects only to the internal Compose service names `postgres` and +`chroma`. Their endpoints are fixed rather than exposed through +`VIDXP_DATABASE_URL`, `VIDXP_CHROMA_SERVER_URL`, or equivalent user settings. + +Optional application services: + +- GPU worker supplementing the CPU worker +- internal CPU-default Ollama `slm` profile +- external OIDC provider configuration + +Postgres, Chroma, and hook endpoints remain private. Only API/MCP and the tus upload +route are published. Long-running services have healthchecks; one-shot storage, +migration, and Chroma-readiness gates must complete before their consumers start. +Required secrets use `${VAR:?required}` rather than insecure defaults. Public +routing cannot reach the tusd callback path; tests exercise that denial +independently of the private-network hook flow. + +Release artifacts are: + +- the native `vidxp` CLI/client package for supported macOS, Linux, and Windows +- an explicit local-worker extra and platform lock for native CPU indexing +- `compose.local.yaml`: an optional containerized Linux evaluation/development + profile; it is not required for native macOS use +- `compose.coolify.yaml`: immutable prebuilt API/MCP, worker, Postgres, Chroma and + tusd images with no build contexts; the same file is valid with ordinary Compose +- `compose.gpu.yaml`: supplemental GPU worker override +- optional `slm` profile + +Use immutable release tags or digests. API/MCP and workers share an application +release but use purpose-specific image targets. API/MCP carries no heavy model or +CUDA dependencies; workers and the optional `slm` service do. + +Deployment v1 is exactly one API/MCP replica and one node. Named +media/artifact/quarantine volumes are part of this topology. The fixed internal +service names and storage paths are not promises of arbitrary external-provider, +multi-replica, or high-availability compatibility. + +The one-click template consumes published tags/digests and must pass a real Coolify +deployment, upgrade, rollback, persistent-data and healthcheck test. + +## 23. Code to retain, move, or remove + +Baseline facts that the rebuild must not mistake for target behavior: + +- project metadata currently requires Python `>=3.10,<3.14`; the target baseline is + the current stable Python release with the supported matrix defined in section 5.1 +- the current Dockerfile/Compose produce one CPU-only Streamlit image/service +- server storage currently uses embedded `chromadb.PersistentClient` +- model loaders use per-module `lru_cache`, not one runtime/resource scheduler +- WhisperX compute type is hardcoded to `float32` +- repository configuration mixes deployment device choice into persistent identity +- the capability registry statically imports every built-in +- `CapabilityDefinition` currently carries CLI and implementation callables +- `clip-anytorch` requires the obsolete `setuptools<81` compatibility pin + +Retain and adapt: + +- capability-specific model/config logic +- frame/audio processing +- FFmpeg/ffprobe helpers +- manifest/checkpoint concepts +- Chroma storage initially +- existing search operations +- CLI rendering and Typer declarations + +Move behind contracts: + +- Chroma access +- repository layout +- media probing and persistence +- model loading/caching +- indexing execution +- actor rendering +- capability registration + +Remove instead of preserving: + +- adapter-owned job state machines +- adapter-owned media stores +- Streamlit process/global persistence policy +- MCP-to-FastAPI internal HTTP calls +- generated OpenAPI-to-MCP mirroring +- environment mutation as application configuration +- module-level application singletons +- progress files as index validity +- destructive in-place active-index replacement +- raw server paths in remote contracts +- caller-selected output paths +- duplicated transport-specific business models +- hardcoded central capability imports as the extension mechanism + +## 24. Verification strategy + +### 24.1 Contract tests + +The same command fixture must produce equivalent CLI JSON, API JSON and MCP +structured results after removing transport-only fields. + +### 24.2 Index durability + +Test: + +- failure before generation commit +- cancellation at each indexing stage +- process death and recovery +- search during replacement +- incremental add/re-index/delete +- atomic active-snapshot switch +- incomplete generation cleanup + +The prior snapshot and affected media generation must remain searchable in every +pre-commit failure. + +### 24.3 Job behavior + +Run the same workflow contract against: + +- SQLite local mode +- Postgres multi-process mode +- queued cancellation +- active cooperative cancellation +- worker restart +- idempotent retry +- CPU and GPU queue routing +- UI/client exit while a local worker continues +- cooperative cancellation latency at every runner checkpoint +- blocking-step cancellation behavior +- stable-executor restart recovery +- release-version drain and recovery + +### 24.4 Media + +Test: + +- resumed upload +- documented bearer-URL behavior and log/referrer redaction +- checksum/deduplication +- invalid extension with real video +- valid extension with invalid content +- interrupted and expired uploads +- duplicate/out-of-order hook delivery +- missed-finish-hook recovery sweep +- abandoned intent/quarantine cleanup +- completion remaining unpublished until durable ffprobe succeeds +- import-root escape +- retained external source disappearance + +### 24.5 MCP + +Validate with the official client over: + +- Streamable HTTP from a separate process +- stdio +- structured output schemas +- union/optional schema fidelity +- auth metadata and audience rejection +- protocol cancellation +- cancellation through the deployed non-buffering proxy +- durable job polling after session/client restart +- bounded actor/search responses +- host/origin rejection and required MCP proxy headers + +### 24.6 Capabilities and artifacts + +Capability discovery tests: + +- no installed plugin +- allowlisted plugin +- disabled plugin is not imported +- duplicate built-in/name +- incompatible contract version +- import failure +- deterministic behavior on supported Python 3.11 through 3.14 + +Managed artifact delivery tests cover authorization, path/symlink containment, +missing or corrupt content, GET/HEAD/range headers, and MIME/disposition metadata. + +### 24.7 Platforms and installation profiles + +Run clean-environment native tests for the latest stable Python on: + +- Apple Silicon macOS +- Linux x86-64 +- Windows x86-64 + +The Apple Silicon gate covers native package installation, FFmpeg/ffprobe, +separate-process DBOS worker recovery, embedded Chroma persistence, scene +CPU/MPS routing, transcription CPU routing, actor ONNX CPU routing, cancellation, +and clean uninstall. It also runs an end-to-end remote-client flow against the +Compose server: resumable media upload, indexing, polling, search, and artifact +retrieval. + +The thin-client environment must import and execute without model, Chroma server, +CUDA, or local-worker dependencies. The local-worker environment must run without +CUDA libraries. Package metadata, platform locks, and release images are tested +from an empty cache so undeclared system or developer-machine dependencies cannot +mask failures. + +### 24.8 CPU/GPU + +Before model downloads: + +- build CPU and GPU targets +- run clean-image `pip check` +- generate SBOM and license/model manifests +- prove the CPU image contains no CUDA runtime +- inspect installed PyTorch/CUDA compatibility +- run CUDA availability/readiness probes +- validate Compose GPU reservation +- validate worker queue routing with a synthetic job +- prove actor work routes to CPU +- validate OOM, cancellation and worker-restart behavior without model downloads + +After CPU functionality is complete: + +- download one pinned model set +- run selected scene and transcription provider smoke tests on a supported GPU +- compare golden outputs against CPU +- record VRAM, precision, throughput and fallback behavior + +## 25. Implementation sequence and gates + +### Phase 1: contracts and composition + +- settings and application factory +- shared identifiers/models/errors +- repository layout +- capability registry cleanup +- lightweight client/core, local-worker, and server dependency groups +- current-Python package metadata and platform lock generation + +Gate: no adapter imports concrete storage/models, cross-adapter schema tests pass, +and the thin-client environment installs and imports without ML/server dependencies. + +### Phase 2: current CPU provider baseline + +- replace obsolete scene, dialogue, and actor provider dependencies +- pin licensed model revisions and record index identities +- model runtime and resource scheduler +- embedded Chroma adapter on the latest stable compatible release +- native CPU packaging for Apple Silicon macOS, Linux x86-64, and Windows x86-64 + +Gate: clean environments on the latest stable Python pass dependency, import, +license, and representative CPU capability smoke tests on all supported platforms. +Apple Silicon also passes any enabled MPS parity test; unsupported acceleration +remains explicit and does not block its CPU path. + +### Phase 3: durable index foundation + +- immutable generations +- authoritative manifests +- immutable multi-media snapshots +- active-snapshot pointer +- repository leases + +Gate: incremental add/re-index/delete works and injected failure/cancellation never +damages the active snapshot. + +### Phase 4: media and artifacts + +- media catalog/store +- local import +- artifact catalog/store +- source playback and snippet contract + +Gate: application operations use IDs, not public filesystem paths. + +### Phase 5: workflow engine + +- DBOS job service +- SQLite local mode +- Postgres worker mode +- typed progress/cancellation + +Gate: restart, cancel and retry tests pass without adapter-owned job state. + +### Phase 6: thin API + +- FastAPI adapter +- auth profiles +- readiness +- small-upload compatibility +- artifact delivery + +Gate: API process performs no indexing/model work and owns no business persistence. + +### Phase 7: remote ingestion and deployment + +- tusd integration +- Coolify Compose +- immutable image targets + +Gate: interrupted multi-part upload resumes and completed media can be indexed by ID. +The gate also covers bearer upload-URL redaction, duplicate/out-of-order hooks, +missed-finish recovery, abandoned-upload retention, and the ffprobe publication +boundary. + +### Phase 8: MCP + +- official SDK v2 server +- Streamable HTTP and stdio +- curated typed tools +- remote auth + +Gate: separate-process remote client completes media-id indexing, polling and search; +schemas and structured results pass conformance checks. + +### Phase 9: multimodal and SLM query + +- fused search +- typed query planning +- evidence-grounded answer synthesis +- deterministic fallback + +Gate: every answer cites retrievable media intervals and invalid plans cannot escape +the operation grammar. + +### Phase 10: GPU + +- GPU worker image/profile +- runtime validation +- pinned model smoke tests +- resource limits + +The 2026-07-29 evaluation selects PyTorch 2.13 `cu126` as the first compatible +NVIDIA baseline. CUDA 12.8 has no Torch 2.13 distribution, while CUDA 13 does +not share current CTranslate2/faster-whisper's CUDA 12 runtime contract. The +full worker set resolves on Python 3.14 to Torch 2.13.0, CTranslate2 4.8.1, +faster-whisper 1.2.1, and cuDNN 9.10.2. Implementation remains deferred until a +CUDA host and prepared model cache are available; no model or image was pulled +during the evaluation. See `docs/deployment/gpu-evaluation.md`. + +Gate: GPU readiness and execution are explicit; no silent CPU fallback. +Clean-image dependency/SBOM checks, CPU-without-CUDA proof, selected scene and +transcription provider smoke tests, CPU actor routing, OOM handling, cancellation, +and worker restart must all pass. + +### Phase 11: desktop + +- desktop adapter +- platform repository lifecycle +- packaging and update strategy + +Gate: desktop uses the same commands/jobs/media/indexes without UI-owned business +logic. + +## 26. Decision validation ledger + +Validated on 2026-07-28: + +- **Official MCP SDK 2.0:** accepted. A separate-process Streamable HTTP probe and a + stdio probe passed tool discovery/calls, Pydantic discriminated unions, optionals, + constraints, `outputSchema`, and `structuredContent`. Required corrections for + mount lifespan, root metadata path, token audience validation, safe errors, + streaming cancellation, host/origin policy, and the MCP-specific body limit are + incorporated above. +- **External FastMCP 3.4.5:** rejected for this phase because its server extra + requires MCP SDK `<2`. The similarly named high-level server in official SDK v2 + is now `MCPServer`; it supplies typed schemas, structured results, Streamable + HTTP, stdio, OAuth resource-server behavior, transport security, and request + limits without generating tools from HTTP routes. +- **fastapi-mcp 0.4:** rejected because it generates an OpenAPI mirror and invokes + the FastAPI application through an internal ASGI HTTP client. That duplicates + transport contracts instead of using VidXP's shared application services. +- **DBOS 2.28:** accepted as durable queue/workflow state, not as process isolation. + A Windows/Python 3.13 probe exercised SQLite queue, progress, stable-executor + crash recovery, and a separate local worker process. Postgres 16 API-client plus + distinct CPU/GPU worker processes and queue routing were exercised. These probes + validate behavior, not the target Python floor or release lock. Active synchronous + cancellation was proven non-preemptive; the separate worker/cooperative semantics + above are therefore mandatory. +- **Current control-plane dependency baseline:** compatible at audit time. + `fastapi==0.140.13`, `mcp==2.0.0`, `dbos==2.28.0`, + `pydantic-settings==2.14.2`, and + `pydantic-ai-slim[openai]==2.19.0` resolved together on Python 3.13. These are + observations, not permanent pins. Implementation re-resolves the latest stable + releases on the latest Python 3.14 maintenance release and records only justified + compatibility exceptions. Release images lock the resulting direct and + transitive artifacts. +- **MCP licensing:** accepted. The audited MCP stack is permissively licensed. +- **DBOS licensing:** accepted with distribution compliance work. DBOS is MIT, but + mandatory psycopg/psycopg-binary dependencies are LGPL-3.0-only. Release artifacts + must carry required notices and satisfy binary redistribution/source obligations. +- **tusd:** accepted with the capability-URL and hook semantics documented above. + HTTP hooks are duplicate/out-of-order capable; the durable importer and sweep own + correctness. +- **Artifacts:** accepted for the managed local content volume. Protected Starlette + range delivery, authorization, storage-key containment, integrity checks, and + response metadata remain mandatory. +- **Pydantic AI/Ollama:** accepted as the local SLM provider boundary. Typed native + output is supported. The exact model remains deliberately blocked by the + no-large-model-download constraint until Phase 9's measured gate. +- **Python and ML compatibility:** the current Python feature release is the + platform baseline. Stable PyTorch, CTranslate2, ONNX Runtime, and OpenCV releases + publish current-Python artifacts for the primary platform set, subject to the + clean-environment runtime gates above. The current stable WhisperX release blocks + that Python baseline and constrains an older Torch family, so it is rejected as + the default transcription provider rather than allowed to hold the platform back. + `faster-whisper`/CTranslate2 is accepted as the CPU transcription provider and + must still pass the Python 3.14, Apple Silicon, transcript, timestamp, and license + release gates. +- **CPU model selection:** accepted without a new multi-model download run. + SigLIP 2 and MIEB provide recent task-matched evidence for the scene encoder; + Qwen3-Embedding publishes stronger multilingual retrieval results than the older + available baselines; faster-whisper publishes CPU/GPU speed and memory comparisons + against Whisper implementations; and OpenCV Zoo publishes the selected detector + and recognizer with evaluation data. VidXP runs regression and compatibility + smoke tests now; a new comparison benchmark is required only when newer evidence + conflicts, deployment measurements regress, or a provider change is proposed. +- **GPU package matrix:** intentionally deferred. The previously proposed Python + 3.11/PyTorch 2.8/WhisperX/CUDA 12.8 matrix is rejected as a target. Phase 10 + resolves the latest mutually compatible stable stack from the then-current CPU + baseline and accepts it only through clean-image, CUDA, model, output-parity, + cancellation, and recovery tests. No model weights or CUDA image were downloaded + during this audit. +- **Chroma deployment:** embedded `PersistentClient` is accepted only locally. + Coolify uses a private Chroma server and client/server adapter. +- **Capability entry points:** accepted as standard trusted-code discovery with the + exact group, allowlist, version and collision rules above. + +Before a release image is published, audit locked direct/transitive packages, +container bases, system libraries, model weights/revisions/quantizations, automatic +alignment/VAD downloads, notices, and redistribution terms. Emit an SBOM and model +manifest for every image. Package metadata alone is not treated as model-license +evidence. diff --git a/docs/benchmarking/README.md b/docs/benchmarking/README.md index bdc0925..cfe692f 100644 --- a/docs/benchmarking/README.md +++ b/docs/benchmarking/README.md @@ -13,8 +13,9 @@ installation and product usage, start with the main | Area | Status | What it means | |---|---|---| | Shared benchmark support | Complete | Stable IDs, time ranges, metadata, top-k retrieval, isolated runs, checkpoints, and prediction files are implemented | -| DiDeMo visual localization | Complete | Full official test run completed over 4,021 queries and 1,037 videos | -| HiREST transcript localization | Validation complete | All 193 official validation pairs were scored; 776 released test predictions are unscored because their public answer bounds are placeholders | +| Guided input preparation | Complete | `vidxp benchmark prepare` estimates and confirms downloads, verifies pinned artifacts, validates DiDeMo media, resumes partial transfers, and prints the runnable benchmark command | +| DiDeMo visual localization | Legacy full result + current smoke | The legacy CLIP stack completed 4,021 official test queries over 1,037 videos; the current SigLIP2 stack passed a one-annotation real execution smoke | +| HiREST transcript localization | Legacy full result + current smoke | The legacy MiniLM stack scored all 193 validation pairs; current Qwen3 passed a two-video real execution smoke; 776 released test predictions remain unscored because their public bounds are placeholders | | LongVALE combined evaluation | Next | Build the visual-plus-speech adapter and validate one evaluation archive before scheduling the full run | | Actor clustering | Data-gated | The preferred BBT/Buffy evaluation still requires lawful access to the source episodes | @@ -44,12 +45,14 @@ they are not the current task list. No single published benchmark covers dialogue retrieval, scene retrieval, and actor clustering together. -The completed DiDeMo and HiREST runs establish separate visual and -transcript-based baselines. LongVALE is the next combined test because it asks a -system to find described events in long videos using visual, speech, and general -audio evidence. VidXP can currently contribute visual and speech evidence; it -does not recognize general sounds such as music, alarms, or barking. Any -LongVALE result must keep that limitation visible. +The retained full DiDeMo and HiREST results establish separate legacy-provider +visual and transcript baselines. Current SigLIP2 and Qwen3 checks establish +adapter/runtime compatibility only; they do not yet provide full-corpus quality +comparisons. LongVALE is the next combined test because it asks a system to find +described events in long videos using visual, speech, and general audio +evidence. VidXP can currently contribute visual and speech evidence; it does +not recognize general sounds such as music, alarms, or barking. Any LongVALE +result must keep that limitation visible. ## Evidence rules diff --git a/docs/benchmarking/adapter_validation.md b/docs/benchmarking/adapter_validation.md index d314e2b..aa92a8c 100644 --- a/docs/benchmarking/adapter_validation.md +++ b/docs/benchmarking/adapter_validation.md @@ -4,23 +4,37 @@ This ledger records the implementation and executable validation of the first two benchmark adapters. Subset runs are smoke tests of the complete data, indexing, serialization, and evaluator path. Their metrics are not paper scores. -For the current scores, metric definitions, and plain-language interpretation, +For the recorded results, metric definitions, and plain-language interpretation, start with [current benchmark results](results.md). This ledger is the technical reproduction record. +The full-corpus scores below are the retained 2026-07-27 legacy-provider +results. The 2026-07-30 SigLIP2/Qwen3 runs are bounded execution smokes, not +replacement quality scores. Both generations used the same physical laptop; +the model and protocol differences are recorded explicitly. + ## Pinned official artifacts | Benchmark | Artifact | Revision | SHA-256 | |---|---|---|---| | DiDeMo | [`data/val_data.json`](https://github.com/LisaAnne/LocalizingMoments/blob/b6a555c8134581305d0ed4716fbc192860e0b88c/data/val_data.json) | `b6a555c8134581305d0ed4716fbc192860e0b88c` | `b0364cc256553332feb19d46bcc4cd2b09774949fe6c0b25e7ed0ff3c6aefebb` | | DiDeMo | [`data/test_data.json`](https://github.com/LisaAnne/LocalizingMoments/blob/b6a555c8134581305d0ed4716fbc192860e0b88c/data/test_data.json) | `b6a555c8134581305d0ed4716fbc192860e0b88c` | `1891c04ec48b3d364c739594b2b6413806b74bd9027c092d896e7ebb930ff1cd` | -| DiDeMo | [`utils/eval.py`](https://github.com/LisaAnne/LocalizingMoments/blob/b6a555c8134581305d0ed4716fbc192860e0b88c/utils/eval.py) | `b6a555c8134581305d0ed4716fbc192860e0b88c` | `4754bb320564e5d2e7c633e0b660e87feca7f00fa73269e50140e81ffb4ca762` | +| DiDeMo | [`utils/eval.py`](https://github.com/LisaAnne/LocalizingMoments/blob/b6a555c8134581305d0ed4716fbc192860e0b88c/utils/eval.py) | `b6a555c8134581305d0ed4716fbc192860e0b88c` | `9ec3e7a171272eb3551b0eaa7bbe9292131ad5cf34fd5c1e02c0fc4a11234df6` | +| DiDeMo | [`data/yfcc100m_hash.txt`](https://github.com/LisaAnne/LocalizingMoments/blob/b6a555c8134581305d0ed4716fbc192860e0b88c/data/yfcc100m_hash.txt) | `b6a555c8134581305d0ed4716fbc192860e0b88c` | `481d9aaf020624d5915200bcf4752fb46d3e1931167e8b46715a5f342577cc4d` | | HiREST | [`data/splits/all_data_val.json`](https://github.com/j-min/HiREST/blob/deffc169b4e8d51c1589d5512ad05da61e81bcee/data/splits/all_data_val.json) | `deffc169b4e8d51c1589d5512ad05da61e81bcee` | `70d32c5fcdffe66cbf3c732dd274f03378da2082f50c9cec7e67705f529ecb4d` | | HiREST | [`data/splits/all_data_test.json`](https://github.com/j-min/HiREST/blob/deffc169b4e8d51c1589d5512ad05da61e81bcee/data/splits/all_data_test.json) | `deffc169b4e8d51c1589d5512ad05da61e81bcee` | `00219050c022ff2fc89c210ca4db605de6aa13c5c6014e4c678345ade3448a62` | | HiREST | [`data/evaluation/categories.json`](https://github.com/j-min/HiREST/blob/deffc169b4e8d51c1589d5512ad05da61e81bcee/data/evaluation/categories.json) | `deffc169b4e8d51c1589d5512ad05da61e81bcee` | `157623d50f7b8482f55fa1c4efc500539784c0399fb2dd60bb687b4006d85ca1` | -| HiREST | [`evaluate.py`](https://github.com/j-min/HiREST/blob/deffc169b4e8d51c1589d5512ad05da61e81bcee/evaluate.py) | `deffc169b4e8d51c1589d5512ad05da61e81bcee` | `c4b8ba9b572ae4088e90ddc3eec2b2cc4f5b4c1a0153ff6e0843817da89a5ca0` | +| HiREST | [`evaluate.py`](https://github.com/j-min/HiREST/blob/deffc169b4e8d51c1589d5512ad05da61e81bcee/evaluate.py) | `deffc169b4e8d51c1589d5512ad05da61e81bcee` | `871b48dc5ce42fbe1a4b672fe4df88a88ce568d57759dfc971e5aacc5f88f119` | | HiREST | [`ASR.zip`](https://huggingface.co/j-min/HiREST-baseline/resolve/54e2f8da7a4384fec8a137011399f5e104069032/ASR.zip) | `54e2f8da7a4384fec8a137011399f5e104069032` | `0b452d38e30064dc7273a58b7b73ec33e307ff83d30048a472777f56e3a29fbc` | +The table records canonical raw upstream bytes. The adapters also accept the +semantically identical CRLF evaluator hashes produced by a Windows Git +checkout: `4754bb320564e5d2e7c633e0b660e87feca7f00fa73269e50140e81ffb4ca762` +for DiDeMo and +`c4b8ba9b572ae4088e90ddc3eec2b2cc4f5b4c1a0153ff6e0843817da89a5ca0` +for HiREST. The observed hash is recorded rather than silently normalizing a +user-supplied checkout. + The adapters verify these hashes before indexing. Each run manifest also records the artifact paths, URLs, revisions, sizes, and observed hashes. VidXP does not replace the official downloaders; the adapters consume prepared media and ASR. @@ -76,8 +90,9 @@ command, working directory, compatibility note, output, and return code. released SRT cues do not contain word timestamps, phrase bounds are interpolated linearly within the real cue bounds and disclosed in the run manifest. -5. Submit the timestamped phrases to the dialogue-only VidXP core. The run uses - MiniLM and does not load WhisperX or decode video. +5. Submit the timestamped phrases to the dialogue-only VidXP core. The legacy + full run used MiniLM; the current smoke used Qwen3 Embedding. Neither path + loads a transcription model or decodes video. 6. Search each prompt only within its known video and retrieve every stored dialogue phrase. Project phrase scores onto one-second bins, assign uncovered seconds an explicit absence penalty, and rank duration-relative windows by @@ -109,9 +124,10 @@ logic remain unchanged, and the shim is disclosed in `evaluator.log`. These choices were made only on official validation data. Test annotations were not used to select either setting. -For DiDeMo, a declared 15-annotation validation subset over four downloadable -official videos was indexed at frame stride `30`. Both alternatives used the -same stored CLIP frame scores and the pinned official evaluator: +For the legacy DiDeMo run, a declared 15-annotation validation subset over four +downloadable official videos was indexed at frame stride `30`. Both +alternatives used the same stored CLIP frame scores and the pinned official +evaluator: | Within-chunk pooling | Rank@1 | Rank@5 | mIoU | |---|---:|---:|---:| @@ -124,11 +140,11 @@ seconds inside the human-selected chunk, but mean pooling diluted it enough to rank chunk 3 first. This is why the change is an aggregation correction rather than a label-specific test patch. -For HiREST, all 193 official validation moment pairs were indexed from released -ASR. The released cues contain no word timestamps, so the repaired core first -created the configured five-word phrases using linear interpolation inside each -real cue. The same stored MiniLM scores were then evaluated over this declared -duration-fraction grid: +For the legacy HiREST run, all 193 official validation moment pairs were +indexed from released ASR. The released cues contain no word timestamps, so the +repaired core first created the configured five-word phrases using linear +interpolation inside each real cue. The same stored MiniLM scores were then +evaluated over this declared duration-fraction grid: | Window fraction | R@0.5 | R@0.7 | |---:|---:|---:| @@ -182,29 +198,97 @@ size match the archive record. The manifest therefore classifies this as an official test result with a documented media substitution, not an untouched official-media run. -## Commands +## 2026-07-30 current-provider regression runs + +These checks used the current CPU providers after the product model swap. They +were intentionally bounded after the integration path was proven; they are not +full benchmark replacements. + +### Hardware and runtime + +| Item | Recorded current setup | +|---|---| +| Laptop | HP ENVY Laptop 16-h0xxx; the same physical hardware used for the legacy runs | +| CPU | Intel Core i7-12700H; 14 cores, 20 logical processors | +| Memory | 15.72 GiB | +| GPU | NVIDIA GeForce RTX 3060 Laptop GPU, 4 GiB; present but unused | +| Runtime | Windows 11; Python 3.14.0; PyTorch 2.13.0+cpu | +| Libraries | Transformers 5.14.1; Sentence Transformers 5.6.1; ChromaDB 1.5.9 | + +HiREST used `Qwen/Qwen3-Embedding-0.6B` at immutable revision +`97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3`. DiDeMo used +`google/siglip2-base-patch16-224` at +`75de2d55ec2d0b4efc50b3e9ad70dba96a7b2fa2`. HiREST consumed the released +SRTs, so the configured faster-whisper model was not loaded or evaluated. + +### Real execution results + +| Run | Declared subset | Result | Runtime evidence | +|---|---|---|---| +| `hirest-qwen3-generation-smoke-20260730` | Two validation prompt/video pairs, including `Make Oatmeal Pancake Mix` / `5V3dI2zp1xA.mp4` | Official evaluator: R@0.5 `50.0`, R@0.7 `50.0`; classification `validation_smoke_test_not_paper_score` | Both media items remained searchable in the run generation; 64.923 recorded stage seconds; 2,593,096-byte store; empty failure log | +| `didemo-siglip2-sample-20260730` | Official test annotation index `0`; one video; 1.0 sample/sec; max chunk pooling | Official evaluator: Rank@1 `0.0`, Rank@5 `1.0`, mIoU `0.0`; classification `smoke_test_not_paper_score` | 29.359 recorded stage seconds; 747,684-byte store; all 21 candidates serialized; empty failure log | + +Prediction SHA-256 values were +`c2cee45eb7802e2c19adb4aec876a8fe2c203c59dde4bb20b41634b83d050afd` +for HiREST and +`4a38ff2c307833393f96fc0af48107a12581ecd9be734d8c0b906b2448a6fd33` +for DiDeMo. The runs were made from commit +`880da785825e95c46bfe050763be42afb02e844f` with the documented uncommitted +benchmark repairs, whose implementation fingerprint was +`afe0ee120f1c7b7daabe6d2c51e320a67a1368158fc5215acc34b7dd3e4e245e`. -Install the optional adapters with the capabilities they evaluate: +### Defects exposed and fixed + +The first current-provider attempt completed all 193 HiREST indexing +checkpoints but failed before predictions because benchmark records lacked the +generation identity now required by the search contract. Adding a shared +generation then exposed that the multi-video runner deleted the entire +generation before each video, leaving only the final video's records. A +two-video real smoke also caught official dataset filenames being passed into +the product's UUID4-only media ID fields. + +The final adapter behavior now: + +- derives stable UUID4-shaped internal media and generation IDs; +- retains official dataset video names at the file/evaluator boundary; +- removes only the current video's records when retrying inside a generation; +- supports documented DiDeMo media substitutions through the public CLI; and +- covers the generation/media-ID boundary in regression tests. + +The temporary 1,037-video corpus, model-run indexes, repository clones, and ASR +copy were removed after recording the evidence above. + +## Prepare and run + +Install the optional adapters and initialize FFmpeg: ```powershell -python -m pip install -e ".[scene,dialogue,benchmarks]" +uv tool install "vidxp[scene,dialogue,benchmarks]" +vidxp init ``` -Run a declared DiDeMo smoke subset by zero-based official annotation indices: +Prepare a declared DiDeMo smoke subset by zero-based official annotation +indices: ```powershell -vidxp benchmark didemo ` - --annotations /data/test_data.json ` - --evaluator /utils/eval.py ` - --media-directory ` - --split test ` - --annotation-indices 0,1,2 ` - --run-id didemo-smoke +vidxp benchmark prepare didemo --split test --annotation-indices 0,1,2 ``` -Omit `--annotation-indices` for the full official test split. +The command reads the pinned metadata needed to inspect the selection, displays +the maximum additional storage, destination free space, and any documented +replacement, then asks before persisting files. It resumes `.part` downloads, +validates each video with +FFprobe and a decoded frame, writes `preparation-manifest.json`, and prints the +complete benchmark command. Omit `--annotation-indices` for the full official +split. + +The known corrupt Multimedia Commons object is never used or replaced +silently. If its annotation is selected, the plan identifies the archived +Wikimedia original, includes its 94,107,862-byte size, and records the SHA-1, +source URL, and generated `media-overrides.json`. `--yes` confirms the displayed +plan for automation; JSON output requires it. -For HiREST, a smoke pair file is a JSON list: +For a HiREST smoke, a pair file is a JSON list: ```json [ @@ -215,23 +299,26 @@ For HiREST, a smoke pair file is a JSON list: ] ``` -Run the declared pair subset: +Prepare the released-ASR archive and a self-contained copy of that selection: ```powershell -vidxp benchmark hirest ` - --ground-truth /data/splits/all_data_test.json ` - --categories /data/evaluation/categories.json ` - --evaluator /evaluate.py ` - --asr-archive /ASR.zip ` - --asr-directory /ASR ` +vidxp benchmark prepare hirest ` --split test ` - --temporal-window-fraction 0.8 ` - --pairs ` - --run-id hirest-smoke + --pairs .\hirest-smoke-pairs.json ``` -Omit `--pairs` to generate all 776 official test predictions. Test output is -explicitly unscored; use `--split validation` with the pinned validation file +Preparation downloads the pinned annotations, categories, evaluator, and +released ASR archive, then extracts only the selected transcripts. No HiREST +video is downloaded by this adapter. Omit `--pairs` to prepare the complete +selected split. + +By default, prepared data is stored below VidXP's platform-native application +data directory under `benchmarks/`. Use `--output-directory` to +choose another volume. The final line is deliberately a copy/paste command, so +the user does not have to assemble artifact paths manually. + +The generated direct command can omit `--pairs` to produce all 776 official +test predictions. Test output is explicitly unscored; use `--split validation` for locally evaluable official metrics. ## Shared run output @@ -252,7 +339,7 @@ They also retain `ground_truth.subset.json`, the core completion marker, and per-video checkpoints. Validation runs add `metrics.json`; held-out HiREST test runs add `submission.summary.json`. Empty failure logs are created deliberately. -## 2026-07-27 executable smoke results +## 2026-07-27 legacy-provider executable smoke results | Adapter | Declared subset | Actual path exercised | Official evaluator result | |---|---|---|---| diff --git a/docs/benchmarking/benchmark_catalog.md b/docs/benchmarking/benchmark_catalog.md index 71aee01..58cd92d 100644 --- a/docs/benchmarking/benchmark_catalog.md +++ b/docs/benchmarking/benchmark_catalog.md @@ -70,18 +70,18 @@ protocol. It does not mean that a similar published score is already comparable. | Category | Benchmark or protocol | Capability actually measured | What a VidXP result would tell us | What it would **not** tell us | Current status | | --- | --- | --- | --- | --- | --- | -| Dialogue | [TVR `sub-only` queries](https://github.com/jayleicn/TVRetrieval) | Subtitle-related paraphrastic query to ranked video intervals, both known-video and corpus-wide | Whether WhisperX/MiniLM transcript evidence can identify and temporally localize relevant TV moments; a subtitle-only run isolates embedding and retrieval | Verbatim quote lookup, scene understanding, actor clustering, generic audio, or end-to-end ASR quality when supplied subtitles are used | Engineering A; raw TV media gated | -| Visual | [TVR `video-only` queries](https://github.com/jayleicn/TVRetrieval) | Visual-language query to ranked video intervals | Whether frame/clip CLIP evidence can retrieve visual moments across a TV corpus | Dialogue retrieval, actor identity, or performance on non-TV domains | Engineering A; raw TV media gated | +| Dialogue | [TVR `sub-only` queries](https://github.com/jayleicn/TVRetrieval) | Subtitle-related paraphrastic query to ranked video intervals, both known-video and corpus-wide | Whether the configured transcript embedding path can identify and temporally localize relevant TV moments; a subtitle-only run isolates embedding and retrieval | Verbatim quote lookup, scene understanding, actor clustering, generic audio, or end-to-end ASR quality when supplied subtitles are used | Engineering A; raw TV media gated | +| Visual | [TVR `video-only` queries](https://github.com/jayleicn/TVRetrieval) | Visual-language query to ranked video intervals | Whether the configured frame/clip embedding path can retrieve visual moments across a TV corpus | Dialogue retrieval, actor identity, or performance on non-TV domains | Engineering A; raw TV media gated | | Combined | [TVR `video+sub` queries](https://github.com/jayleicn/TVRetrieval) | Queries annotators judged to need both visual and subtitle evidence | Whether fixed late fusion improves corpus moment retrieval when both implemented paths contain useful evidence | Generic sound understanding, actor clustering, learned cross-modal reasoning, or intended `video+sub` coverage from a dialogue-only ablation | Engineering A with fixed fusion; raw TV media gated | -| Speech-backed instructional | [HiREST](https://github.com/j-min/HiREST) | Instructional goal to ranked videos, one relevant interval, moment segmentation, and step captioning | Whether VidXP's chunking, MiniLM embeddings, vector index, and interval selection retrieve semantically relevant spoken procedural content | Verbatim dialogue search, entertainment-video generalization, actors, WhisperX accuracy in released-ASR mode, or full video retrieval when negative candidates lack ASR | Known-video validation complete; released test predictions unscored because public bounds are placeholders; full video retrieval gated | +| Speech-backed instructional | [HiREST](https://github.com/j-min/HiREST) | Instructional goal to ranked videos, one relevant interval, moment segmentation, and step captioning | Whether VidXP's chunking, dialogue embeddings, vector index, and interval selection retrieve semantically relevant spoken procedural content | Verbatim dialogue search, entertainment-video generalization, actors, transcription accuracy in released-ASR mode, or full video retrieval when negative candidates lack ASR | Legacy MiniLM validation complete; current Qwen3 two-video smoke complete; released test predictions unscored because public bounds are placeholders; full video retrieval gated | | Narrated retrieval | [QuerYD](https://www.robots.ox.ac.uk/~vgg/data/queryd/) | Paragraph text↔video retrieval and narration text↔localized-clip ranking over supplied ground-truth proposals | Whether VidXP visual representations rank the correct narrated video or oracle segment proposal once source media are processed | In-scene conversational dialogue, overlapping speech, unrestricted boundary prediction, or VidXP quality from the released Collaborative Experts features | Protocol/reference artifacts ready; true VidXP run raw-media gated; narration audio unresolved | | Ranked search | [TVR-Ranking](https://huggingface.co/axgroup/TVR-Ranking) | Graded ranking of multiple relevant corpus moments for imprecise queries | Whether VidXP orders several partially relevant moments usefully, not merely whether its top result overlaps one answer | ASR quality when subtitles are supplied, actors, or generalization outside TVR | Engineering A after TVR; media/license gated | | Speech-backed whole-video | [How2R](https://aclanthology.org/2020.emnlp-main.161/) | Instructional-video retrieval using video plus aligned speech/subtitles, introduced with HERO | Whether VidXP can rank instructional clips from transcript and scene evidence under HERO's retrieval setup | Conversational dialogue, temporal boundary prediction, actors, or immediate executability before artifacts/licenses are rechecked | Relevant benchmark; current artifact/access status unresolved | | Spoken retrieval | [TREC Podcasts](https://trecpodcasts.github.io/) | Semantic query to graded, timestamped long-form spoken segments | Whether VidXP can rank relevant spoken passages across a large audio corpus | Visual retrieval, video timing beyond fixed segments, or actor clustering | Reference-only; corpus access closed | | Spoken occurrence | [NIST OpenKWS](https://www.nist.gov/document/openkws13-evalplan-v4pdf) | Detection of every exact spoken keyword occurrence with calibrated accept/reject decisions | Miss, false-alarm, timing, threshold-calibration, indexing-time, and search-time behavior for exact terms | Semantic/paraphrase dialogue retrieval, scene search, or actor performance | Engineering A; Babel/LDC data gated | | Spoken/visual archive | [SAVA, MediaEval 2015](https://ceur-ws.org/Vol-1436/Paper11.pdf) | Spoken-plus-visual query to unrestricted BBC archive intervals | Whether combined evidence retrieves useful broadcast segments | Actor clustering or reproducibility on presently obtainable public media | Official task-overview paper verified; blocked/reference-only | -| Multilingual temporal | [mTVR](https://aclanthology.org/2021.acl-short.92/) | English/Chinese multilingual TV moment retrieval | Whether a multilingual retrieval stack preserves temporal retrieval across those two languages | Urdu support, current MiniLM multilingual quality, or actor clustering | Reference; not an intentional current claim | -| Visual moment | [DiDeMo](https://github.com/LisaAnne/LocalizingMoments) | Natural-language query to one of 21 fixed moments in a known video | Whether CLIP frame scores and deterministic five-second aggregation rank the human-selected moment | Corpus-wide search, variable boundary quality, dialogue, or actors | Adapter and full official test run complete | +| Multilingual temporal | [mTVR](https://aclanthology.org/2021.acl-short.92/) | English/Chinese multilingual TV moment retrieval | Whether a multilingual retrieval stack preserves temporal retrieval across those two languages | Urdu support, current Qwen3 multilingual quality, or actor clustering | Reference; not an intentional current claim | +| Visual moment | [DiDeMo](https://github.com/LisaAnne/LocalizingMoments) | Natural-language query to one of 21 fixed moments in a known video | Whether configured scene-embedding scores and deterministic five-second aggregation rank the human-selected moment | Corpus-wide search, variable boundary quality, dialogue, or actors | Legacy CLIP full official test complete; current SigLIP2 one-annotation smoke complete | | Visual moment/highlight | [QVHighlights](https://github.com/jayleicn/moment_detr) | Query to ranked variable-duration moments and saliency on an official two-second clip grid | Whether zero-shot visual similarity finds relevant intervals and assigns useful clip-level saliency | Dialogue or actor performance; parity with supervised temporal models; validity of a point prediction under tIoU | Engineering A; storage/compute gated; exact test-label release must be named | | Visual moment | [Charades-STA](https://github.com/jiyanggao/TALL) | Query to ranked intervals in short indoor activity videos | Whether fixed-grid CLIP scoring localizes described actions in short videos | Corpus search, speech, actors, or broad-domain generalization | Engineering A; data agreement gated; original and filtered splits must not be mixed | | Whole-video visual | [MSR-VTT](https://www.microsoft.com/en-us/research/publication/msr-vtt-a-large-video-description-dataset-for-bridging-video-and-language/) | Text-to-whole-video ranking under a declared retrieval split | Whether pooled VidXP scene embeddings retrieve the correct short video from a corpus | Timestamp localization, dialogue retrieval, actors, or comparability across the 9k/1k and 7k/1k/2k conventions | Engineering A; media preparation and exact split required | @@ -113,7 +113,7 @@ protocol. It does not mean that a similar published score is already comparable. | Face verification provenance | [LFW / dlib protocol](https://github.com/ageitgey/face_recognition) | Same/different identity verification on still-image pairs | Only the provenance and generic discrimination of the underlying face embedding | VidXP actor clustering, face detection coverage, temporal continuity, or unknown-K performance | Not an actor benchmark; provenance only | | Combined temporal | [LongVALE](https://github.com/ttgeng233/LongVALE) | Vision, speech, and generic-audio event grounding in long videos, with one interval per event query | Whether a frozen VidXP interval proposal and scene/speech fusion localize multimodal events | Actor clustering or full omni-modal coverage because VidXP lacks generic-audio recognition | Engineering A/medium; artifacts reachable; compliance, interval, and runtime gates remain | | Audiovisual retrieval | [FLARE](https://flarebench.github.io/) | Caption-to-clip/video retrieval plus clip-level model-simulated visual-only, audio-only, and joint query retrieval | Where VidXP's scene, speech, and fixed-fusion clip rankings succeed or fail across evidence types | Actor performance, human-authored-query generalization, or full generic-sound capability | Engineering A/medium; ready/watchlist preprint | -| Large-corpus event | [MultiVENT 2.0](https://huggingface.co/datasets/hltcoe/MultiVENT2.0) | Multilingual event-centric ranked-video retrieval using visual, speech/ASR, embedded-text/OCR, and human-description metadata evidence | Whether existing VidXP visual, speech, and metadata paths scale to a large heterogeneous corpus; unsupported OCR becomes a measurable weakness | Timestamp localization, actor clustering, generic acoustic-event retrieval, or multilingual adequacy of current MiniLM | Engineering A with large operational adapter; approximately 1.93 TB gated | +| Large-corpus event | [MultiVENT 2.0](https://huggingface.co/datasets/hltcoe/MultiVENT2.0) | Multilingual event-centric ranked-video retrieval using visual, speech/ASR, embedded-text/OCR, and human-description metadata evidence | Whether existing VidXP visual, speech, and metadata paths scale to a large heterogeneous corpus; unsupported OCR becomes a measurable weakness | Timestamp localization, actor clustering, generic acoustic-event retrieval, or multilingual adequacy of the current Qwen3 embedding provider | Engineering A with large operational adapter; approximately 1.93 TB gated | | Large-corpus shot | [TRECVID AVS / V3C](https://www-nlpir.nist.gov/projects/tv2025/avs.html) | Natural-language query to up to 1,000 ranked master shots, measured with mean xinfAP | Whether VidXP scene retrieval scales to over a million pooled/judged shots and returns useful corpus rankings | Dialogue or actor performance, or comparable latency across different hardware | Archived 2024/2025 protocol; agreement and approximately 1.6 TB gated | | Multimodal whole-video | [MUVR](https://github.com/debby-0527/MUVR) | Paired query-video plus detailed text to ranked short normalized videos; pure-text and pure-video are ablations | Whether VidXP ranks relevant videos for a declared pure-text ablation or a reusable-CLIP visual slice | Timestamp localization, audio/dialogue understanding, actors, or equivalence to the defining paired-query task | Ablation slice adaptable; lower priority | | Audiovisual whole-video | [VALOR-32K](https://github.com/TXH-mercury/VALOR) | Bidirectional audiovisual-text retrieval and audiovisual captioning over 32,000 ten-second clips with human captions | Whether VidXP's visual/speech or fixed audiovisual representation ranks captioned clips on a standard released split | Temporal localization, actor clustering, long-video search, or full parity without a generic-audio model | Benchmark and official artifacts verified; media links/source rights must be checked | @@ -520,7 +520,8 @@ The evaluator, not VidXP, should compute the published metrics. path, not a standalone reproduction package. That path additionally needs its base model/projector/stage weights, CUDA environment, and evaluator. In all cases, those features bypass VidXP and cannot substitute for running VidXP's - CLIP/WhisperX/MiniLM encoders on the raw evaluation videos. + configured scene/transcription/dialogue providers on the raw evaluation + videos. - **Operations:** raw evaluation media are now packaged directly, so YouTube survival is no longer the access gate. The raw MP4 payload expands to 43,662,117,997 bytes (40.664 GiB); retaining both raw ZIPs and extracted MP4s diff --git a/docs/benchmarking/core_contract.md b/docs/benchmarking/core_contract.md index e7581e8..6a2897e 100644 --- a/docs/benchmarking/core_contract.md +++ b/docs/benchmarking/core_contract.md @@ -33,8 +33,10 @@ does not change merely because the same run is relocated to another output or storage directory. Checkpoint filenames are hashes of video IDs, so official IDs cannot accidentally become platform-specific paths. -The CLI and Streamlit interface use `chroma_data/` as their local run. Indexes -created with an older schema must be rebuilt; VidXP does not invent missing end +The CLI and Streamlit interface use `repositories/default/` beneath the +operating system's per-user VidXP data root. This local application repository +is separate from the explicit `benchmark_runs/` output above. Indexes created +with an older schema must be rebuilt; VidXP does not invent missing end timestamps, video IDs, or source IDs. ## Indexing API @@ -46,27 +48,35 @@ from vidxp.core.runner import run_index config = IndexConfig( dataset="example", split="test", - run_id="clip-stride-5", + run_id="scene-1fps", + generation_id="123456781234423481234567890abcde", enabled_modalities=("scene",), - frame_stride=5, - capability_options={"scene": {"batch_size": 32}}, + capability_options={ + "scene": {"batch_size": 32, "sample_fps": 1.0}, + }, storage_batch_size=256, ) manifest = run_index( [ - VideoSource(video_id="video-001", path="videos/001.mp4"), - VideoSource(video_id="video-002", path="videos/002.mp4"), + VideoSource( + video_id="223456781234423481234567890abcde", + path="videos/001.mp4", + ), + VideoSource( + video_id="323456781234423481234567890abcde", + path="videos/002.mp4", + ), ], config, ) ``` -Released timestamped ASR can be indexed without WhisperX or video decoding: +Released timestamped ASR can be indexed without running a transcription model or decoding video: ```python source = VideoSource( - video_id="video-001", + video_id="223456781234423481234567890abcde", transcript=( {"text": "first timestamped span", "start": 0.0, "end": 2.4}, {"text": "second timestamped span", "start": 2.4, "end": 5.0}, @@ -77,20 +87,22 @@ config = IndexConfig( dataset="example", split="test", run_id="released-asr", + generation_id="423456781234423481234567890abcde", enabled_modalities=("dialogue",), ) run_index([source], config) ``` -Scene-only runs do not load WhisperX or face recognition. Supplied-transcript +Scene-only runs do not load a transcription or actor model. Supplied-transcript dialogue runs load the dialogue encoder but do not decode video. Actor-only runs -do not load CLIP or WhisperX. CLIP inference, dialogue encoding, and Chroma writes -use their configured batch sizes. Cancellation is cooperative and is checked -between batches. The Streamlit process exposes cancellation for indexing workers -it started and reports that the current batch must finish first. The shared file -lock also lets the UI detect an active CLI or separate-process run, although one -process cannot cancel a run owned by another process. +do not load scene or transcription models. Scene inference, dialogue encoding, +and Chroma writes use their configured batch sizes. Cancellation is cooperative +and is checked between batches. The Streamlit process exposes cancellation for +indexing workers it started and reports that the current batch must finish +first. The shared file lock also lets the UI detect an active CLI or +separate-process run, although one process cannot cancel a run owned by another +process. Scene and actor indexing share one video probe and one sampled-frame stream when both are enabled. Each materialized frame is converted to RGB once and then fed @@ -98,23 +110,29 @@ to independently sized scene and actor batches. Actor detections are written incrementally; clusters below `actor_min_detections` are removed after clustering instead of retaining every detection in process memory. -`frame_stride` controls which frames are retrieved into Python and sent through -the models. Skipped frames are advanced with the video backend without being -materialized. Inter-frame codecs may still require backend-internal decoding, so -stride is not claimed to reduce codec work in direct proportion to the stride. -Manifests distinguish `source_frames_advanced`, unique `sampled_frames`, and the -sum of per-modality `frame_operations`. +Scene `sample_fps` is converted from the probed source FPS into a deterministic +frame cadence. A source below the requested rate uses every available frame +without duplication. `frame_stride` independently controls actor and legacy +visual capability sampling. When scene and actor are both enabled, the decoder +materializes the union of their required frames once and routes each capability +only its own cadence. Inter-frame codecs may still require backend-internal +decoding, so sampling is not claimed to reduce codec work in direct proportion +to the cadence. Manifests distinguish `source_frames_advanced`, unique +`sampled_frames`, and the sum of per-modality `frame_operations`. ## Stored identity and metadata -Every record has a deterministic escaped source ID: +Generation-aware records have a deterministic escaped source ID: ```text -run_id:video_id:modality:local_id +generation_id:run_id:video_id:modality:local_id ``` Each component is encoded before joining, so a colon or Unicode character inside -an official ID cannot collide with the separator. +source metadata cannot collide with the separator. Product media and generation +IDs are lowercase UUID4 hex. Dataset adapters retain official video keys at +their input/evaluator boundary and deterministically map them to valid internal +IDs. - Dialogue records store text, start/end, phrase ID, video ID, modality, source ID, dataset, split, and run ID. @@ -146,7 +164,7 @@ for hit in result.hits: ) ``` -Passing `video_id="video-001"` restricts retrieval to one video; omitting it +Passing a media UUID through `video_id` restricts retrieval to one video; omitting it searches the run corpus. Results are deterministically ordered by raw distance and then source ID. Scene vectors and, by default, dialogue vectors are normalized before the explicitly configured Chroma distance (`vector_distance`, default diff --git a/docs/benchmarking/research_papers.md b/docs/benchmarking/research_papers.md index 005c9fd..fdc5d2c 100644 --- a/docs/benchmarking/research_papers.md +++ b/docs/benchmarking/research_papers.md @@ -120,7 +120,7 @@ ranks whole videos rather than locating timestamps. | Paper | Typical benchmarks | Relevance | | --- | --- | --- | -| [CLIP: Learning Transferable Visual Models From Natural Language Supervision](https://arxiv.org/abs/2103.00020) | Image-text transfer only; no selected video temporal benchmark | Foundation of the current scene encoder; provenance, not a video benchmark | +| [CLIP: Learning Transferable Visual Models From Natural Language Supervision](https://arxiv.org/abs/2103.00020) | Image-text transfer only; no selected video temporal benchmark | Foundation for the legacy CLIP scene baseline and a relevant SigLIP2 predecessor; provenance, not a video benchmark | | [CLIP4Clip](https://github.com/ArrowLuo/CLIP4Clip) | MSR-VTT, MSVD, LSMDC, ActivityNet, DiDeMo whole-video/clip retrieval | Established CLIP video-text comparator; its DiDeMo/ActivityNet use is not temporal localization | | [Frozen in Time](https://github.com/m-bain/frozen-in-time) | MSR-VTT, DiDeMo, LSMDC, MSVD whole-video retrieval | Retrieval baseline and practical MSR-VTT preparation route; WebVid/image-caption sets are pretraining | | [X-CLIP](https://github.com/xuguohai/X-CLIP) | MSR-VTT, MSVD, LSMDC, DiDeMo, ActivityNet whole-video retrieval | Stronger supervised CLIP-style comparator, not a timestamp baseline | diff --git a/docs/benchmarking/results.md b/docs/benchmarking/results.md index a2da9a9..ea5e233 100644 --- a/docs/benchmarking/results.md +++ b/docs/benchmarking/results.md @@ -1,4 +1,4 @@ -# Current benchmark results +# Benchmark results This page answers three questions: @@ -9,13 +9,59 @@ This page answers three questions: Detailed artifacts, hashes, commands, and evaluator behavior remain in the [adapter validation ledger](adapter_validation.md). -## Results at a glance +## Evidence at a glance -| Benchmark | Evaluated data | VidXP result | Status | -|---|---|---|---| -| DiDeMo | Official test: 4,021 searches over 1,037 videos | Rank@1 **20.19%**, Rank@5 **55.71%**, average overlap **34.60%** | Valid official test baseline with one documented replacement for a corrupt copy of an official video | -| HiREST | Official validation: 193 known-video searches | R@0.5 **78.24%**, R@0.7 **44.56%** | Validation result; the prediction-window setting was selected on this same validation set | -| HiREST | Released test: 776 known-video searches | Predictions generated, no score | Public test boundaries are placeholders, so local scoring would be meaningless | +| Generation | Benchmark | Evaluated data | Result | What it supports | +|---|---|---|---|---| +| Legacy full | DiDeMo | Official test: 4,021 searches over 1,037 videos | Rank@1 **20.19%**, Rank@5 **55.71%**, mean IoU **34.60%** | Full visual baseline with one documented replacement for a corrupt official media object | +| Legacy full | HiREST | Official validation: 193 known-video searches | R@0.5 **78.24%**, R@0.7 **44.56%** | Validation baseline; the 0.8 prediction-window fraction was selected on this same validation set | +| Legacy full | HiREST | Released test: 776 known-video searches | Predictions generated, not scored | Public test boundaries are placeholders, so local scoring would be meaningless | +| Current smoke | DiDeMo | Official test annotation index `0`; one video | Rank@1 **0**, Rank@5 **1**, mean IoU **0** | Real SigLIP2 execution, serialization, and official-evaluator check only | +| Current smoke | HiREST | Two declared validation pairs over two videos | R@0.5 **50**, R@0.7 **50** | Real Qwen3 execution, multi-video storage, filtered search, serialization, and official-evaluator check only | + +The current-provider rows are deliberately tiny regression runs. Their +percentages are not quality estimates and must not be compared with the full +legacy rows. A current full-corpus score has not been run. + +## Runtime and model generations + +The legacy and current checks used the same physical laptop, as confirmed for +this rerun: + +- HP ENVY Laptop 16-h0xxx; +- Intel Core i7-12700H, 14 cores and 20 logical processors; +- 15.72 GiB system memory; +- NVIDIA GeForce RTX 3060 Laptop GPU with 4 GiB VRAM present but unused; +- CPU-only PyTorch execution. + +| Generation | Dialogue embedding | Scene embedding | Sampling/window | Transcription in these benchmarks | +|---|---|---|---|---| +| Legacy full, 2026-07-27 | `all-MiniLM-L6-v2` | OpenAI CLIP `ViT-B/32` through `clip-anytorch` | HiREST 0.8-duration window; DiDeMo fixed 30-frame stride and max chunk pooling | Released HiREST SRTs; WhisperX `large-v2` was not exercised | +| Current smoke, 2026-07-30 | `Qwen/Qwen3-Embedding-0.6B` at `97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3` | `google/siglip2-base-patch16-224` at `75de2d55ec2d0b4efc50b3e9ad70dba96a7b2fa2` | HiREST same 0.8-duration window; DiDeMo source-aware 1.0 sample/sec and max chunk pooling | Released HiREST SRTs; faster-whisper `large-v3-turbo` was not exercised | + +The surviving legacy run record did not pin immutable model revisions or a +complete package/OS snapshot. The current smoke manifests record Windows 11, +Python 3.14.0, PyTorch 2.13.0+cpu, Transformers 5.14.1, and +Sentence Transformers 5.6.1. This limitation is stated instead of inventing +legacy revision metadata after the fact. + +## Multimodal comparison contract + +Natural-language answer prose is not an official benchmark prediction format. +Benchmark runs continue to preserve atomic scene and dialogue hits, raw +distances, and the existing dataset serializers. When a dataset contains both +eligible modalities, reports must show three fixed rows: + +| Retrieval path | What is compared | +|---|---| +| Scene only | The existing visual retrieval output | +| Dialogue only | The existing transcript retrieval output | +| Fixed RRF fusion | Overlap-connected intervals ranked with `rrf_v1`, `k=60` | + +No fused benchmark score is reported until the same frozen dataset inputs and +evaluator used by the atomic rows have been run. Generated `QueryAnswer` claims +remain a separate grounding evaluation and cannot replace these retrieval +comparisons. ## What the measurements mean @@ -48,13 +94,13 @@ overlap = 8 / 12 = 0.67 ## DiDeMo -### Comparison +### Legacy full-result comparison | Method | Rank@1 | Rank@5 | Mean IoU | |---|---:|---:|---:| | Chance | 3.75% | 22.50% | 22.64% | | Common-position guess | 19.40% | 66.38% | 26.65% | -| **VidXP** | **20.19%** | **55.71%** | **34.60%** | +| **VidXP legacy CLIP baseline** | **20.19%** | **55.71%** | **34.60%** | | Published MCN trained on DiDeMo | 28.10% | 78.21% | 41.08% | The common-position guess ignores the search text and video content. It ranks @@ -66,7 +112,7 @@ DiDeMo-trained MCN system remains better on all three measurements. ### Honest conclusion -VidXP produces meaningful visual localization, but its candidate ranking is not +The legacy VidXP stack produces meaningful visual localization, but its candidate ranking is not yet competitive with the published trained system. Improving how scene evidence is combined across nearby frames is the clearest DiDeMo improvement target. @@ -77,12 +123,12 @@ and its checksum are recorded in the ## HiREST -### Comparison +### Legacy full-result comparison | Method on the same 193 validation pairs | R@0.5 | R@0.7 | |---|---:|---:| | Return almost the entire video without using the query | 68.91% | 23.83% | -| **VidXP transcript search** | **78.24%** | **44.56%** | +| **VidXP legacy MiniLM transcript search** | **78.24%** | **44.56%** | | Improvement | **+9.33 points** | **+20.73 points** | VidXP turns the transcript matches into a score over the video's timeline and @@ -102,7 +148,9 @@ or superiority over published HiREST systems because: - the released test answers contain placeholder time ranges and cannot be scored locally. -The result is a useful validation baseline, not a final held-out paper result. +The result is a useful legacy validation baseline, not a final held-out paper +result. The current two-video Qwen3 smoke establishes compatibility only; it +does not supersede this score. ## Next benchmark: LongVALE diff --git a/docs/benchmarking/runtime_validation.md b/docs/benchmarking/runtime_validation.md index f18b4ae..49ce5c7 100644 --- a/docs/benchmarking/runtime_validation.md +++ b/docs/benchmarking/runtime_validation.md @@ -4,7 +4,47 @@ This ledger records executable checks for the benchmark-ready core. It is separate from unit-test coverage and from benchmark results. A smoke result here must not be reported as a paper score. -## 2026-07-27 Chunk 1 closure +## 2026-07-30 current-provider benchmark closure + +Two real, bounded runs validated the current CPU providers and official +evaluator paths after the model swap: + +| Check | Executed path | Observed result | +|---|---|---| +| HiREST multi-video smoke | Two released-ASR validation pairs; Qwen3 Embedding 0.6B; Chroma; filtered known-video search; 0.8-duration ranking; pinned evaluator | Both media remained indexed and searchable; two predictions serialized; R@0.5/R@0.7 `50/50`; empty failure log | +| DiDeMo sample | Official test annotation index `0`; SigLIP2; source-aware 1.0 sample/sec; max chunk pooling; pinned evaluator | One prediction containing all 21 candidates serialized; Rank@5 `1.0`; empty failure log | +| Targeted regression suite | Runner, benchmark adapters/CLI, search, storage unit and integration tests | 67 tests and 4 subtests passed | + +The percentages are smoke-subset outputs, not current full-corpus quality +scores. The runs used the same HP ENVY/i7-12700H/15.72 GiB laptop as the legacy +results. PyTorch 2.13.0+cpu did not use the installed RTX 3060 Laptop GPU. + +The executable pass found and fixed three integration defects before closure: + +- benchmark records lacked the now-required generation identity; +- multi-video retries deleted an entire shared generation instead of only the + current video's records; and +- official dataset filenames were still being supplied to UUID4-only product + media fields. + +## 2026-07-30 benchmark-preparation closure + +The guided acquisition path was exercised independently of model loading and +indexing: + +| Check | Executed path | Observed result | +|---|---|---| +| DiDeMo preparation | Official test annotation `0`; pinned raw annotations/evaluator/hash map; one Multimedia Commons video | 5.3 MiB maximum-additional-storage plan shown; download completed; FFprobe and frame decode passed; manifest and runnable command emitted | +| DiDeMo resumability | Repeated the same preparation directory | Zero new files and zero additional bytes; existing checksums and media validation passed | +| HiREST preparation | Complete validation moment set; pinned metadata and released ASR | 7.2 MiB downloaded; the plan now reserves up to 17.2 MB for transcript extraction; 193 selected transcripts extracted; manifest and runnable command emitted | + +Both temporary preparation directories were removed after validation. This pass +also found that the earlier evaluator checksums described CRLF Windows checkout +bytes rather than canonical raw GitHub bytes. Preparation now pins canonical +raw hashes while direct adapter invocation accepts and records either canonical +LF or the known CRLF checkout form. + +## 2026-07-27 legacy-provider Chunk 1 closure ### Failure that triggered the pass diff --git a/docs/benchmarking_research.md b/docs/benchmarking_research.md index e6769e5..d960bd2 100644 --- a/docs/benchmarking_research.md +++ b/docs/benchmarking_research.md @@ -63,7 +63,7 @@ The `face_recognition` project states **99.38% accuracy on Labeled Faces in the Use a separate “published context” table, followed by the paper's own “same-harness baselines” table. The latter should contain only methods rerun on the identical frozen VidXP test set, for example: - dialogue: random, exact lexical/BM25, reference-transcript oracle, and full WhisperX pipeline; -- scene: random, CLIP with each pre-registered frame stride, and any temporal aggregation variant; +- scene: random, CLIP with each pre-registered time-based sample rate, and any temporal aggregation variant; - actors: all-singleton, all-in-one, fixed 0.55 threshold, and development-tuned threshold. State underneath the published-context table: **“Values are reproduced from their source papers and are not directly comparable to VidXP because datasets, languages, retrieval units, supervision, relevance definitions, and hardware differ.”** This prevents a literature-review table from being mistaken for an empirical leaderboard. @@ -74,7 +74,7 @@ Use an immutable, versioned corpus and publish a manifest containing video IDs, Use three disjoint splits: -- **Development:** select face threshold, five-word phrase length, frame stride, temporal de-duplication, similarity settings, and any rejection threshold. +- **Development:** select face threshold, five-word phrase length, scene sample rate, temporal de-duplication, similarity settings, and any rejection threshold. - **Test:** run once after the protocol and parameters are frozen. - **Optional training:** only if a component is trained or fine-tuned. VidXP's present models are otherwise evaluated zero-shot. @@ -244,7 +244,7 @@ Record peak: PyTorch's `max_memory_allocated()` returns the peak tensor memory since program start and documents resetting peak statistics between stages ([official API](https://docs.pytorch.org/docs/stable/generated/torch.cuda.memory.max_memory_allocated.html)). Python exposes maximum RSS through `resource.getrusage` ([official documentation](https://docs.python.org/3/library/resource.html)); NVIDIA documents sampled GPU utilization and frame-buffer memory semantics in `nvidia-smi` ([official documentation](https://docs.nvidia.com/deploy/nvidia-smi/index.html)). -For every result table, report CPU model/core count, RAM, storage, GPU and VRAM, OS, accelerator driver/runtime, Python and dependency versions, model identifiers/hashes, precision/compute type, batch size, thread counts, input resolution/fps, frame stride, database settings, and Git commit. Fix seeds where randomness exists, keep the machine otherwise idle, randomize condition order, and publish raw per-run measurements and the benchmark script. +For every result table, report CPU model/core count, RAM, storage, GPU and VRAM, OS, accelerator driver/runtime, Python and dependency versions, model identifiers/hashes, precision/compute type, batch size, thread counts, input resolution/fps, effective scene sample rate, actor frame stride, database settings, and Git commit. Fix seeds where randomness exists, keep the machine otherwise idle, randomize condition order, and publish raw per-run measurements and the benchmark script. ## 6. Aggregation, uncertainty, and comparisons @@ -253,13 +253,13 @@ For every result table, report CPU model/core count, RAM, storage, GPU and VRAM, - Report 95% confidence intervals by resampling the highest independent unit—source title/video, not adjacent frames or queries from the same event. Use the same resampled units for paired system comparisons. - Publish per-query/per-video scores so alternative aggregation is possible. - Compare systems on the identical frozen queries and judgments. -- Present quality and efficiency together: e.g. \(R@1\) versus RTF for Whisper model/compute type, scene frame stride, and face threshold/history choices. +- Present quality and efficiency together: e.g. \(R@1\) versus RTF for Whisper model/compute type, scene sample rate, and face threshold/history choices. ## 7. Minimum paper table set 1. Corpus and annotation statistics by split and query stratum. 2. Dialogue ASR/alignment results, then end-to-end retrieval with reference-transcript oracle and lexical baseline. -3. Scene retrieval by query type, within-video versus corpus-wide, with frame-stride ablation. +3. Scene retrieval by query type, within-video versus corpus-wide, with sample-rate ablation. 4. Actor detection coverage plus B-cubed P/R/F1, pairwise P/R/F1, ARI, and cluster-count diagnostics. 5. Indexing RTF/stage breakdown, query cold/warm p50/p95 and QPS, peak RAM/accelerator memory, and index size. 6. Paired 95% confidence intervals and qualitative failure examples selected by a fixed rule (for example, worst five test queries per module), not hand-picked successes. diff --git a/docs/desktop.md b/docs/desktop.md new file mode 100644 index 0000000..11ca697 --- /dev/null +++ b/docs/desktop.md @@ -0,0 +1,145 @@ +# Desktop application + +The desktop application is a Tauri v2 launcher, first-run configuration +surface, and process supervisor. The operating-system package installs the +application; the application then provisions its local processing runtime. It +does not contain a second VidXP implementation: + +- the selected capability extras are installed from the exact configured + package release; +- the Streamlit browser interface is an optional installation surface; +- repositories and models use the same platform VidXP data directory as the + CLI, while managed Python and package environments use private desktop + application-data directories; +- the existing DBOS worker remains the durable execution boundary; and +- closing the desktop process stops both Streamlit and the repository worker. + +The desktop separates shared product data from its private implementation +state. The shared root is the same operating-system VidXP data directory used +by the CLI: + +```text +VidXP/ + repositories/ + default/ + models/ # default; a user-selected directory is also supported +``` + +The identifier-scoped private desktop directory contains only the managed +runtime and desktop state: + +```text +dev.grayhat.vidxp/ + runtimes/ + python/ + active-runtime.json +``` + +On Windows the per-user NSIS package installs program files under +`%LOCALAPPDATA%\Programs\VidXP`, keeping them separate from both directories. +The desktop's uv download cache uses the operating system-provided app-cache +directory. Docker and Compose storage remains explicitly volume-backed and does +not inherit this desktop layout. + +The application bundles `uv` and performs a system-media preflight before +creating Python or downloading VidXP. If FFmpeg is missing on Windows or +macOS, the application uses a native operating-system dialog to show the exact +approved package-manager command and obtain consent before running it. Linux +shows the applicable terminal command without trying to automate elevation. +The application verifies ffmpeg, ffprobe, +`libx264`, and `aac`, then installs the exact Python and VidXP versions in +`desktop/runtime-manifest.json`, persists the verified absolute executable +paths through `vidxp init`, runs the full `vidxp doctor`, and only then activates +the new runtime. A failed or cancelled setup never replaces the previous active +runtime. + +## Build locally + +From `desktop/`: + +```powershell +npm ci +npm run sidecar:windows +npm run desktop:dev +npm run desktop:build +``` + +On macOS Apple Silicon or Linux x86-64: + +```bash +npm ci +npm run sidecar:unix +npm run desktop:dev +npm run desktop:build +``` + +The sidecar scripts download the pinned official `uv` release, verify its +checked-in SHA-256 archive digest from `desktop/sidecars.json`, and place the +target-suffixed binary where Tauri expects it. The Rust build also executes the +target sidecar and rejects a version mismatch. Generated binaries and build +outputs are ignored by Git. `desktop:build` selects NSIS on Windows, DMG on +macOS, and AppImage on Linux, disables signing, and requires the checked-in +Cargo lock to remain unchanged. + +## First-run configuration + +Users select dialogue, scene, and actor capabilities independently. Interfaces +are selected separately: the browser interface adds the `frontend` extra only +when selected. Model preparation can be deferred, and a native folder picker +can select a model-cache directory before any model is downloaded. +For the current beta, it first acquires only the exact VidXP package from +TestPyPI with dependency resolution disabled. It then resolves that installed +package's selected extras from production PyPI. This prevents TestPyPI from +becoming a competing source for transitive dependencies. The acquisition index +is derived from the stamped package version: prereleases use TestPyPI and stable +versions use production PyPI. +Windows and Linux resolve CPU-only PyTorch wheels using uv's +`--torch-backend cpu`; macOS uses native PyPI wheels. The custom PyTorch index +is therefore a resolver input and is not embedded as a package URL, avoiding +the prior package-publication failure mode. Every selected profile is also +constrained by `desktop/runtime-constraints.txt`, exported from the repository +lock for the complete local-worker and frontend dependency set. Capability +selection controls which packages are installed; the constraints prevent those +packages from drifting independently after the desktop binary is published. + +Optional model preparation invokes the shared `vidxp prepare` command for only +the selected modalities. Setup subprocesses are owned by the Tauri supervisor; +closing the app cancels the active process and stops a preparation worker before +exit. No model is bundled in the installer. + +After a runtime is configured, the Tauri supervisor starts hidden in the system +tray. A browser-enabled profile opens the local interface in the operating +system's default browser. **Open VidXP** reuses the already-running interface; +**Quit VidXP** runs the full supervised shutdown for the interface and +repository worker. Closing the configuration/status window hides it to the tray +instead of terminating a configured runtime. + +The native NSIS, DMG, and AppImage packages themselves never install FFmpeg or +run a package manager. That consented action belongs to first-run configuration, +where errors and retries are visible. `vidxp doctor` remains a read-only validation +gate. Target FFmpeg binaries will not be bundled until their exact build +provenance, enabled codecs, and redistribution licenses are recorded. This is +an explicit packaging gate, not a silent download from an unaudited third +party. + +## Release targets + +The initial target matrix is: + +- Windows x86-64: per-user NSIS installer; +- macOS Apple Silicon: DMG; and +- Linux x86-64: AppImage. + +The release workflow builds all three from the stamped release commit and +attaches them to the matching GitHub release. Initial packages are unsigned; +Windows SmartScreen and macOS Gatekeeper may therefore require explicit user +confirmation. Signing and notarization improve that experience but do not +block publication. + +The Tauri shell itself uses the operating system webview. Selecting no browser +surface omits Streamlit and its Python dependencies, but does not turn the +Tauri executable into a non-webview application. Users who require no webview +at all should install a CLI/MCP package profile instead of the desktop package. + +Updater integration, richer tray controls, in-app playback, CUDA installers, +and true offline installers remain deferred. From bb9b68b39b4c008b411ee5dcf53632d1e8566d4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:53:41 +0000 Subject: [PATCH 7/8] chore(release): prepare main --- .release-please-manifest.json | 4 ++-- CHANGELOG.md | 15 +++++++++++++++ desktop/CHANGELOG.md | 7 +++++++ desktop/VERSION | 2 +- desktop/package-lock.json | 4 ++-- desktop/package.json | 2 +- desktop/runtime-manifest.json | 4 ++-- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- pyproject.toml | 2 +- 11 files changed, 34 insertions(+), 12 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3185f6..d3e3c4c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,4 @@ { - ".": "0.2.1-b.1", - "desktop": "0.2.1-b.1" + ".": "0.3.0-b.1", + "desktop": "0.3.0-b.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 697f552..4ad9c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # CHANGELOG +## [0.3.0-b.1](https://github.com/grayhatdevelopers/vidxp/compare/v0.2.1-b.1...v0.3.0-b.1) (2026-07-31) + + +### Features + +* **deploy:** add published server deployment profiles ([600e333](https://github.com/grayhatdevelopers/vidxp/commit/600e3335bfd5b0dd97406b4c66dbb4b291db2049)) +* **desktop:** add managed local application ([3ff20ec](https://github.com/grayhatdevelopers/vidxp/commit/3ff20ec9aebf4bea7efe041b0ae9ca2e637266c9)) +* **interfaces:** expose CLI, browser, API, and MCP surfaces ([5ae9fc7](https://github.com/grayhatdevelopers/vidxp/commit/5ae9fc7bbb54dfcbca4a7597604d8466dc3b978c)) +* **platform:** add durable media and search services ([98b85a8](https://github.com/grayhatdevelopers/vidxp/commit/98b85a8d06e181919d724716b616bebdd63ecd24)) + + +### Documentation + +* present product setup capabilities and roadmap ([8d4d5ef](https://github.com/grayhatdevelopers/vidxp/commit/8d4d5effda8b00a113315a287c31490a43b212d0)) + ## v0.2.0 (2026-07-28) ### Features diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 9d38040..02047b7 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.3.0-b.1](https://github.com/grayhatdevelopers/vidxp/compare/desktop-v0.2.1-b.1...desktop-v0.3.0-b.1) (2026-07-31) + + +### Features + +* **desktop:** add managed local application ([3ff20ec](https://github.com/grayhatdevelopers/vidxp/commit/3ff20ec9aebf4bea7efe041b0ae9ca2e637266c9)) + ## 0.2.1-b.1 Initial desktop preview. diff --git a/desktop/VERSION b/desktop/VERSION index b0eba74..3e581f5 100644 --- a/desktop/VERSION +++ b/desktop/VERSION @@ -1 +1 @@ -0.2.1-b.1 +0.3.0-b.1 diff --git a/desktop/package-lock.json b/desktop/package-lock.json index b6972e3..191ee06 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "vidxp-desktop", - "version": "0.2.1-b.1", + "version": "0.3.0-b.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vidxp-desktop", - "version": "0.2.1-b.1", + "version": "0.3.0-b.1", "devDependencies": { "@tauri-apps/cli": "2.11.4" } diff --git a/desktop/package.json b/desktop/package.json index e83472d..367ff60 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "vidxp-desktop", "private": true, - "version": "0.2.1-b.1", + "version": "0.3.0-b.1", "type": "module", "scripts": { "tauri": "tauri", diff --git a/desktop/runtime-manifest.json b/desktop/runtime-manifest.json index 306c32b..0451135 100644 --- a/desktop/runtime-manifest.json +++ b/desktop/runtime-manifest.json @@ -1,8 +1,8 @@ { "schema_version": 1, - "desktop_version": "0.2.1-b.1", + "desktop_version": "0.3.0-b.1", "package_name": "vidxp", - "package_version": "0.2.1-b.1", + "package_version": "0.3.0-b.1", "dependency_index": "https://pypi.org/simple", "dependency_constraints_sha256": "d3ed16906841952017f112903356bfe433a8515a37aa3f56e4af4dfa1f985835", "python_version": "3.14.6", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 4ca0eda..da8a989 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -4373,7 +4373,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vidxp-desktop" -version = "0.2.1-b.1" # x-release-please-version +version = "0.3.0-b.1" # x-release-please-version dependencies = [ "atomic-write-file", "hex", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 846c719..02230a0 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vidxp-desktop" -version = "0.2.1-b.1" # x-release-please-version +version = "0.3.0-b.1" # x-release-please-version description = "The VidXP desktop launcher and local runtime supervisor" edition = "2024" rust-version = "1.97" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 208ff38..b46cc67 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "VidXP", - "version": "0.2.1-b.1", + "version": "0.3.0-b.1", "identifier": "dev.grayhat.vidxp", "build": { "frontendDist": "../web" diff --git a/pyproject.toml b/pyproject.toml index 23cfc77..3fc3371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vidxp" -version = "0.2.1-b.1" +version = "0.3.0-b.1" authors = [ { name = "Muhammad Haroon" }, { name = "Talha Momin" }, From 83891e3115f507ddc751415accb98f457a8bffa6 Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Fri, 31 Jul 2026 12:11:36 +0500 Subject: [PATCH 8/8] fix(release): resolve repository in finalizers (#30) --- .github/workflows/publish-desktop.yml | 15 ++++++++++++--- .github/workflows/release-to-pypi.yml | 9 +++++++-- .github/workflows/release-to-test-pypi.yml | 5 ++++- tests/test_packaging.py | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 1ef287a..2fd0baa 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -112,11 +112,19 @@ jobs: run: | tag="${{ inputs.tag }}" version="${tag#desktop-v}" - gh release upload "$tag" desktop-dist/* --clobber + gh release upload "$tag" desktop-dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --clobber if [[ "$version" == *-* ]]; then - gh release edit "$tag" --draft=false --prerelease + gh release edit "$tag" \ + --repo "$GITHUB_REPOSITORY" \ + --draft=false \ + --prerelease else - gh release edit "$tag" --draft=false --latest + gh release edit "$tag" \ + --repo "$GITHUB_REPOSITORY" \ + --draft=false \ + --latest fi - name: Synchronize stable source back to main @@ -125,5 +133,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh workflow run sync-channels.yml \ + --repo "$GITHUB_REPOSITORY" \ --ref main \ -f release_sha="${{ needs.verify.outputs.source_sha }}" diff --git a/.github/workflows/release-to-pypi.yml b/.github/workflows/release-to-pypi.yml index 5a3688f..cf54687 100644 --- a/.github/workflows/release-to-pypi.yml +++ b/.github/workflows/release-to-pypi.yml @@ -161,13 +161,18 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | - gh release upload "${{ inputs.tag }}" dist/* --clobber - gh release edit "${{ inputs.tag }}" --draft=false + gh release upload "${{ inputs.tag }}" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --clobber + gh release edit "${{ inputs.tag }}" \ + --repo "$GITHUB_REPOSITORY" \ + --draft=false - name: Synchronize stable source back to main env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh workflow run sync-channels.yml \ + --repo "$GITHUB_REPOSITORY" \ --ref main \ -f release_sha="${{ needs.build.outputs.source_sha }}" diff --git a/.github/workflows/release-to-test-pypi.yml b/.github/workflows/release-to-test-pypi.yml index de2f463..c9d0529 100644 --- a/.github/workflows/release-to-test-pypi.yml +++ b/.github/workflows/release-to-test-pypi.yml @@ -144,7 +144,10 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | - gh release upload "${{ inputs.tag }}" dist/* --clobber + gh release upload "${{ inputs.tag }}" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --clobber gh release edit "${{ inputs.tag }}" \ + --repo "$GITHUB_REPOSITORY" \ --draft=false \ --prerelease diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 9090c21..bdc4c9f 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -435,6 +435,21 @@ def test_release_please_preserves_desktop_manifests_and_links_versions(self): release_workflow, ) + publisher_repo_flags = { + "release-to-test-pypi.yml": 2, + "release-to-pypi.yml": 3, + "publish-desktop.yml": 4, + } + for workflow, expected in publisher_repo_flags.items(): + contents = ( + ROOT / ".github" / "workflows" / workflow + ).read_text(encoding="utf-8") + self.assertEqual( + contents.count('--repo "$GITHUB_REPOSITORY"'), + expected, + workflow, + ) + desktop_publish = ( ROOT / ".github" / "workflows" / "publish-desktop.yml" ).read_text(encoding="utf-8")

    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%0A07x2*2Pgo@y(&ZRa`Hb(Q=t5L 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 B1#EiXgeFBWgcguq#gU>E0R=-Sjv|bs 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 z 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 R-_rm9002ovPDHLkV1iYbZ{Gj_ literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..aae1a36e0abaad6db2d7437b73cafb4465c276de GIT binary patch literal 244327 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 z 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 rdNHxY~qOhSgNrc6%0pF zU?o*hq{>^+bLzAb;dg353bZIePwh}eu!Q974IvpRwAus{n|8F%e*~PZqEkQri}+o9 zTR+#|Qgb1*h}HRZAB2oRO^^>p42Y3G{!K3#D3*ZM5;uDze4X{R5nH)$!UV$Gsyl$t z42b~nGOy3%2JdAI^H$Z+&!3>D7TnrPm(PSl=8;DRsq0{2{h_wM2&O!SM3g|ypFBTI z;Y?a}&UvqmuDeL&Gs|9*CldQDlAOOLBsga3UwiFX<(BNX-~O6p>HAH5WYpKkby;T! zRlzF&RUrALbw{fzbqloZ8 z?j`Y|Afg4p>y{Oh*nZ?Xo>DERH|MWFI5vy??tnW|OOy7#ko&M^V#c7U=mqp8h zO%Z^tk(dVQhae4mmT@L-3pQIr3_3V`hr;0W!>P{b(^1(f>C3YErtAMwaq)8kD2tHn z9d<1n(t?ufmU4Hr_H&_2utt9m+VH&0^dgjVhJ4f0l7@A!}Nulp(*bxKPV-{r;WHxBiWRSkG0Vs!tr3E`2^GM`+L5E&9I?0Tv0l@REg zVy3>EBx2v-`6MGGBEa~ZHxdZ$hqdiAkT0~LV-YYOWyVYFA>2QV-{J~s_eq}v(iuyc zJT*EVyg#png)qltx(jLRel@wZ&!-SwvG!dTv3<_UE!qPKMJGYjhu%GId?t{#xHyuj z3jXo9{Co34v7me{+wNS1rUrCZ;|~n{>{A=~cZa~y?h;9w^&0kj7KRzQY>x_UU7+nm z{?bGsY+Ng?a8T*z(F$)1!)mUV<;$oyjJ^^QeeSJuhu%m9wjmjdTodYp`Ms zrZ;Po(HKA2!}f^*5ejLT^*{vsjIzVmDiQ(7C*62_<1?N7@hHHTv<}Gr3I0}fxde8$ zgn)C*rbczi&)Dn`I`R|d_}|n`6VsVfPdhc3uE>UD^2<2{+3y9GyrMwnaUBDw&@m_{ z1yW_OjF99CadJ0%QlPJC?kfd|Z@hQ?p`{8ju~0{i!6_s~jJa_y+qfmYX!sIn#X}5B zgRhY?Q(lS9^ru`H6TQ(Efxya7KJlak%VTxMn;^|K0M)N!A=pdqkL6w6hkie+PF>=2A+ek4`>kQib;#TmM44L;$A)hgTgtG@opQX*{F-#| z$c%huBrH3lGMd5V1_8TdHp*w!R2cVxXTT<7uwl#KZrt!jY$hQ|e|Jp`B&{6i(A<1{ z|8;T>d2dF5ApaQ=%@0(U0bLnu0D*(Sb8#r!d!M}&6@c>qGn;T+5dP&L^;F zi_1@18|z6-JUhZ_P9C(1@WrSp@RS5K2#2ShdU|t&RJbH)>(=iP6V9eLc`!>`I31_u zz9G?IjS)1MVxbj)&jarrdd*`^P-%Ja8LC#ggZD@hH(b!&V6CCJ$T_=2WV1t?9>W;Pwo9PS_l#co1Sxj&N=64WxTX?XB-`M1sXYu;zcXU z>wwVrh_nD+5-D!@5U6}~6|7k%3 zN+|jdF|R)rA)5Ml*9>t>ZPVcR$yk-?DvK*e06N#anB(eVr_UW0?6ThQ?fvCl70Jj{ z&J4 zpPh37pR5dj21xcelVdJJ`>de6)Tkz=hXDa_m@8}6INNCW3Go>tYrT8QRmOl_=Q!^CJgkB8+`FpES&BBeDLe&VJ|lTu z=bDx%X!jn~AErvjN-(7jMPQ@H%yj@Q2#Hlti9~y1M=QWHYDs$K*P8;(-+kBJS{bsb z({=#1hcJq4wT!5s(1JQ`uq&>-QW^4))9x0tD2RhF?O~~-n~%Yb zji!wTM~HoWhj2lrnYB#MA9-E$Deje4*gf~$t%w+-gGb5q1soNfdtX>=37IbGg)hhR zdfs_o)N_2pFMgqa9)7rtuO7KZ5gSt#?4^1!QCB|$d4EP=`COYs8(>da%a@AT4+25# zQEu#dd|pKXBi{(v{*1HE)Zh&vkVIrpK~@e_677N}8@;`|=}4jw8bNwqh)#b@1kl^( zI8r1nGs|;9_Ln^Y67O+h7tDl(sz8c^Cu5BuIwVyqnJL*Y`L6HAl?QL1tF<*bR_FV* zfNz>I^KgZ2cH%5t?0G&2!Wpk=MeKlgF=)C!5d(LN9Q!UL7~KtTaOIi^otmfx8yN7S zRWw$)S>+HmwNY#P+Em=*(l?x>J;b(hug#u!RVt*QZE`HYlfe1u_+e9-!=`_?BO(AM zB4a(bfLyq062#Bft_jl($j*|fQ#n!%p&&LI`xg55uHR2@XMyEdgWT)0y5ZS)Wyct| zpfu6y6U0Dei>1d1%NrIrz5i2XLk9viVwRC7b4d@s`g_FB8>;7j;aUwdJ@`+n^mpN= zt~mIGL=@C1XaEpw;8;+>viSl7qxa6}9qppEQJIotb1$h7fuY}j|GoCQ1!I|R1l1;I zY>beqoB%=3naUkSKZt@Pk%~%4Z(o(@V%OGHEZEC0bJrx$1*N$~ttLq}fJaC^T$?Dc8>w`TehrD0$SvG`4?Qov)(mpK0qmkE-AwgL~huiv5%lo3oB7T_~xwg2}NO%x>@+ zEdUk)gr_vHTMGaP(#U}jm^x_J_mY6x*p;$Sd=72dOW2j&9ytJO~rBAwEg-@N$)F_f7N!hhhDh) z&!!3XxO!IMdwqFdF(jK3{Hv9H?flI{ICO1NH;Sf1JP@q~?hpFvD(oa1^W7qV^SIc^ z8f4GCb3m-n*zoE56Uf-Cz3=A#Ylr|OJz4?;DQ4oobm4`HI8a3@LX_P%zGt^xo?hRQ z^+s*$jbxBygf@`|m8YLl+vs2Z@)mvUo{?ofnfS_jADFc$H}g1WzeSP??#;90v+ODN zCt^W}2B|R9d1DpXX?aTC=eK4Q{^1_}j5y*!D8Bvn+k{i_fXXlvl*i*qM6g^UWYmd| zu{IbD1cpCJ0w&LWLr|9|<$95OiIpIU>FQ04wSnW@mOvoe=48oj?wgudSpf32bPL|;J z(MpD*gr87X$ws&Lm7@~Bx1J27Fm$*^bzB#4h+m{!@RlL5)^ zYQ26x%h;!!GN|Yb^i-=>+a35g2z1MSY=XPCf);lIdi|f`j%TmoSJ%HPq z$-edyH6IL2_S^3>iY_cf?Ro+dmC`xqoUO1WL4U{AP+e)yKZTF|Y;`_2g#XX%MOOg% zQZn2R+mY3Re=|5KkVHf`1FC5I0&Y5@P}hfU&1Xg^GzyxuugJ^>&49hNsz&k^hCc)? ztFDbRz`c*GG|4atCBH{6p6gEAfHklb`7PXMv~%v2VDY*jK~$Sq&8jW$e*5XY3<1oz zHKD!c2NI?HyajFot)NDa3wYToP-fmAmuVTsL-{-;kc{uKeztzNwMR(w{XTH--XH+; zkPY%)^ugKqAC~qBy==}8lQoe2=#`UP@k#`2r{lEY*l{4lh=|q!YZ9eEdhsLxB-e;* zt;7k8az9hSt(cr+)I&Sr5m8FSR++VfSWy_EoT$`qZDG(lK&kLdX(J#5{R#p@a-K-~ z;0Lg$OkzTpY6GzUsP4a>Qwzo73B+8w@Fw65uEWB95oj?!imINvwsr01Jh`zI7hmlHvU_=RO?E~b~#=Z}W$ zfXk}COaiLXa&rX#X$isZuF7}y`Ky@)-qjJJ(xGfxm(LF*10AzJ^wind2>!aDrGjPr zEa-TTTJLll8!`bAUBIy)Xk@)C2S`3^H*cLx6f^^sp&1TE;SQ}jv$vHYh1z@&Yc4}9?l&>ADEO;%31=DLc8=3 z!-(ZJ-OSlwab}}`%MfCIeJ%!0SPaVXTlcRUHV^ zhY`L`r($e!0Db`954s1m@&pnl;b6kUjIey$7HbAbnLW{3?ahb48r1i=KQ9uJD5g=j z-Xb&a65K%Uc>Xb;n<%QJK{+b|rcEMrf9LZA)M1Oh4pRrbC)@*wfo2HAeEe8ty?rN2 zmGFLe9TF#I@o5841>bVZFGXnV3RURD+S%sneX&mXIp>-^>HII9ul$@}i%Aa&4{HS; z(x}iBNcT1pEzb90hkaJtYUm2Q@@lZ=h6_LfLqfKk_2R7+&_p8gPW_zdfxv?RMc|u% zn>UH8So)e9BY$EFbc7^5B!_4_y#4#PwT)rZP-P3X8vF;m1KO0qMKmGlNqWY9ya}y0 zZZHre_`)o0kaSLH5(}@Y*PVSnS=%P(Tb;v~nR?*dxk?(y!AQ zUtyxLnUQkP^bU1^a6o7r*&QPUSl zhWEvEf;<

    j+Q#VPt{0;hCTy{6pVpr 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ɘa@+t5ejE(n@Axq6M)atd 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