From b615d5ace0fb4cfd9e07817b4b96da8d5d762684 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:45:27 +0530 Subject: [PATCH 01/13] Squash feature/doc-transform --- README.md | 1 + backend/Dockerfile | 7 +- ...6fd_create_doc_transformation_job_table.py | 40 + ...dd_source_document_id_to_document_table.py | 31 + backend/app/api/docs/documents/upload.md | 19 +- backend/app/api/main.py | 2 + .../app/api/routes/doc_transformation_job.py | 58 + backend/app/api/routes/documents.py | 86 +- backend/app/core/doctransform/registry.py | 115 ++ backend/app/core/doctransform/service.py | 128 ++ .../app/core/doctransform/test_transformer.py | 15 + backend/app/core/doctransform/transformer.py | 13 + .../core/doctransform/zerox_transformer.py | 45 + backend/app/crud/__init__.py | 1 + backend/app/crud/doc_transformation_job.py | 84 ++ backend/app/models/__init__.py | 3 +- backend/app/models/doc_transformation_job.py | 30 + backend/app/models/document.py | 40 + .../documents/test_route_document_info.py | 1 + .../documents/test_route_document_list.py | 1 + .../test_route_document_permanent_remove.py | 1 + .../documents/test_route_document_remove.py | 3 +- .../documents/test_route_document_upload.py | 217 +++- .../api/routes/test_doc_transformation_job.py | 357 ++++++ .../core/doctransformer/test_service/base.py | 128 ++ .../doctransformer/test_service/conftest.py | 71 ++ .../test_service/test_execute_job.py | 250 ++++ .../test_service/test_execute_job_errors.py | 178 +++ .../test_service/test_integration.py | 167 +++ .../test_service/test_start_job.py | 156 +++ .../test_crud_collection_create.py | 2 +- .../test_crud_collection_read_all.py | 2 +- .../tests/crud/test_doc_transformation_job.py | 230 ++++ backend/app/tests/utils/document.py | 4 +- backend/pyproject.toml | 3 +- backend/uv.lock | 1131 +++++++++++++---- 36 files changed, 3364 insertions(+), 256 deletions(-) create mode 100644 backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py create mode 100644 backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py create mode 100644 backend/app/api/routes/doc_transformation_job.py create mode 100644 backend/app/core/doctransform/registry.py create mode 100644 backend/app/core/doctransform/service.py create mode 100644 backend/app/core/doctransform/test_transformer.py create mode 100644 backend/app/core/doctransform/transformer.py create mode 100644 backend/app/core/doctransform/zerox_transformer.py create mode 100644 backend/app/crud/doc_transformation_job.py create mode 100644 backend/app/models/doc_transformation_job.py create mode 100644 backend/app/tests/api/routes/test_doc_transformation_job.py create mode 100644 backend/app/tests/core/doctransformer/test_service/base.py create mode 100644 backend/app/tests/core/doctransformer/test_service/conftest.py create mode 100644 backend/app/tests/core/doctransformer/test_service/test_execute_job.py create mode 100644 backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py create mode 100644 backend/app/tests/core/doctransformer/test_service/test_integration.py create mode 100644 backend/app/tests/core/doctransformer/test_service/test_start_job.py create mode 100644 backend/app/tests/crud/test_doc_transformation_job.py diff --git a/README.md b/README.md index 0dab99f1..cf840ce7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - [docker](https://docs.docker.com/get-started/get-docker/) Docker - [uv](https://docs.astral.sh/uv/) for Python package and environment management. +- **Poppler** – Install Poppler, required for PDF processing. ## Project Setup diff --git a/backend/Dockerfile b/backend/Dockerfile index 99db91d2..f348afe5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,8 +7,11 @@ ENV PYTHONUNBUFFERED=1 # Set working directory WORKDIR /app/ -# Install system dependencies -RUN apt-get update && apt-get install -y curl +# Install system dependencies (added poppler-utils) +RUN apt-get update && apt-get install -y \ + curl \ + poppler-utils \ + && rm -rf /var/lib/apt/lists/* # Install uv package manager COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/ diff --git a/backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py b/backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py new file mode 100644 index 00000000..dec2d783 --- /dev/null +++ b/backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py @@ -0,0 +1,40 @@ +"""create doc transformation job table + +Revision ID: 9f8a4af9d6fd +Revises: b5b9412d3d2a +Create Date: 2025-08-29 16:00:47.848950 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '9f8a4af9d6fd' +down_revision = 'b5b9412d3d2a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('doc_transformation_job', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('source_document_id', sa.Uuid(), nullable=False), + sa.Column('transformed_document_id', sa.Uuid(), nullable=True), + sa.Column('status', sa.Enum('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', name='transformationstatus'), nullable=False), + sa.Column('error_message', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['source_document_id'], ['document.id'], ), + sa.ForeignKeyConstraint(['transformed_document_id'], ['document.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('doc_transformation_job') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py b/backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py new file mode 100644 index 00000000..8220d918 --- /dev/null +++ b/backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py @@ -0,0 +1,31 @@ +"""add source document id to document table + +Revision ID: b5b9412d3d2a +Revises: 40307ab77e9f +Create Date: 2025-08-29 15:59:34.347031 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = 'b5b9412d3d2a' +down_revision = '40307ab77e9f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('document', sa.Column('source_document_id', sa.Uuid(), nullable=True)) + op.create_foreign_key(None, 'document', 'document', ['source_document_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'document', type_='foreignkey') + op.drop_column('document', 'source_document_id') + # ### end Alembic commands ### diff --git a/backend/app/api/docs/documents/upload.md b/backend/app/api/docs/documents/upload.md index a0276578..a6d8bd42 100644 --- a/backend/app/api/docs/documents/upload.md +++ b/backend/app/api/docs/documents/upload.md @@ -1,2 +1,17 @@ -Upload a document to the AI platform. The response will contain an ID, -which is the document ID required by other routes. +Upload a document to the AI platform. + +- If only a file is provided, the document will be uploaded and stored, and its ID will be returned. +- If a target format is specified, a transformation job will also be created to transform document into target format in the background. The response will include both the uploaded document details and information about the transformation job. + +### Supported Transformations + +The following (source_format → target_format) transformations are supported: + +- pdf → markdown + - zerox + +### Transformers + +Available transformer names and their implementations, default transformer is zerox: + +- `zerox` \ No newline at end of file diff --git a/backend/app/api/main.py b/backend/app/api/main.py index 9d3ae489..f8cac63f 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -5,6 +5,7 @@ assistants, collections, documents, + doc_transformation_job, login, organization, openai_conversation, @@ -26,6 +27,7 @@ api_router.include_router(collections.router) api_router.include_router(credentials.router) api_router.include_router(documents.router) +api_router.include_router(doc_transformation_job.router) api_router.include_router(login.router) api_router.include_router(onboarding.router) api_router.include_router(openai_conversation.router) diff --git a/backend/app/api/routes/doc_transformation_job.py b/backend/app/api/routes/doc_transformation_job.py new file mode 100644 index 00000000..802f1fd3 --- /dev/null +++ b/backend/app/api/routes/doc_transformation_job.py @@ -0,0 +1,58 @@ +import logging +from uuid import UUID +from fastapi import APIRouter, HTTPException, Query, Path as FastPath +from app.models import DocTransformationJob, DocTransformationJobs +from app.crud.doc_transformation_job import DocTransformationJobCrud +from app.utils import APIResponse +from app.api.deps import SessionDep, CurrentUserOrgProject + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/documents/transformations", tags=["doc_transformation_job"]) + + +@router.get( + "/{job_id}", + description="Get the status and details of a document transformation job.", + response_model=APIResponse[DocTransformationJob], +) +def get_transformation_job( + session: SessionDep, + current_user: CurrentUserOrgProject, + job_id: UUID = FastPath(description="Transformation job ID"), +): + crud = DocTransformationJobCrud(session, current_user.project_id) + job = crud.read_one(job_id) + return APIResponse.success_response(job) + + +@router.get( + "/", + description="Get the status and details of multiple document transformation jobs by IDs.", + response_model=APIResponse[DocTransformationJobs], +) +def get_multiple_transformation_jobs( + session: SessionDep, + current_user: CurrentUserOrgProject, + job_ids: str = Query(..., description="Comma-separated list of transformation job IDs"), +): + job_id_list = [] + invalid_ids = [] + for jid in job_ids.split(","): + jid = jid.strip() + if not jid: + continue + try: + job_id_list.append(UUID(jid)) + except ValueError: + invalid_ids.append(jid) + + if invalid_ids: + raise HTTPException( + status_code=422, + detail=f"Invalid UUID(s) provided: {', '.join(invalid_ids)}", + ) + + crud = DocTransformationJobCrud(session, project_id=current_user.project_id) + jobs = crud.read_each(set(job_id_list)) + jobs_not_found = set(job_id_list) - {job.id for job in jobs} + return APIResponse.success_response(DocTransformationJobs(jobs=jobs, jobs_not_found=jobs_not_found)) \ No newline at end of file diff --git a/backend/app/api/routes/documents.py b/backend/app/api/routes/documents.py index ee9a0843..b3ccbf5e 100644 --- a/backend/app/api/routes/documents.py +++ b/backend/app/api/routes/documents.py @@ -1,17 +1,26 @@ import logging from uuid import UUID, uuid4 -from typing import List +from typing import List, Optional from pathlib import Path -from fastapi import APIRouter, File, UploadFile, Query, HTTPException +from fastapi import APIRouter, File, UploadFile, Query, Form, BackgroundTasks, HTTPException from fastapi import Path as FastPath +from fastapi.responses import JSONResponse +from fastapi import HTTPException from app.crud import DocumentCrud, CollectionCrud -from app.models import Document, DocumentPublic, Message +from app.models import Document, DocumentPublic, Message, DocumentUploadResponse, TransformationJobInfo from app.utils import APIResponse, load_description, get_openai_client from app.api.deps import CurrentUser, SessionDep, CurrentUserOrgProject from app.core.cloud import get_cloud_storage from app.crud.rag import OpenAIAssistantCrud +from app.core.doctransform import service as transformation_service +from app.core.doctransform.registry import ( + get_file_format, + is_transformation_supported, + get_available_transformers, + resolve_transformer +) logger = logging.getLogger(__name__) router = APIRouter(prefix="/documents", tags=["documents"]) @@ -36,13 +45,48 @@ def list_docs( @router.post( "/upload", description=load_description("documents/upload.md"), - response_model=APIResponse[DocumentPublic], + response_model=APIResponse[DocumentUploadResponse], ) -def upload_doc( +async def upload_doc( session: SessionDep, current_user: CurrentUserOrgProject, src: UploadFile = File(...), + background_tasks: BackgroundTasks = None, + target_format: str | None = Form( + None, + description="Desired output format for the uploaded document (e.g., pdf, docx, txt). " + ), + transformer: str | None = Form( + None, + description="Name of the transformer to apply when converting. " + ), ): + # Determine source file format + try: + source_format = get_file_format(src.filename) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + # validate if transformation is possible or not + if target_format: + if not is_transformation_supported(source_format, target_format): + raise HTTPException( + status_code=400, + detail=f"Transformation from {source_format} to {target_format} is not supported" + ) + + # Resolve the transformer to use + if not transformer: + transformer = "default" + try: + actual_transformer = resolve_transformer(source_format, target_format, transformer) + except ValueError as e: + available_transformers = get_available_transformers(source_format, target_format) + raise HTTPException( + status_code=400, + detail=f"{str(e)}. Available transformers: {list(available_transformers.keys())}" + ) + storage = get_cloud_storage(session=session, project_id=current_user.project_id) document_id = uuid4() @@ -54,8 +98,36 @@ def upload_doc( fname=src.filename, object_store_url=str(object_store_url), ) - data = crud.update(document) - return APIResponse.success_response(data) + source_document = crud.update(document) + + + job_info: TransformationJobInfo | None = None + if target_format and actual_transformer: + job_id = transformation_service.start_job( + db=session, + current_user=current_user, + source_document_id=source_document.id, + transformer_name=actual_transformer, + target_format=target_format, + background_tasks=background_tasks, + ) + job_info = TransformationJobInfo( + message=f"Document accepted for transformation from {source_format} to {target_format}.", + job_id=str(job_id), + source_format=source_format, + target_format=target_format, + transformer=actual_transformer, + status_check_url=f"/documents/transformations/{job_id}" + ) + + document_schema = DocumentPublic.model_validate(source_document, from_attributes=True) + document_schema.signed_url = storage.get_signed_url(source_document.object_store_url) + response = DocumentUploadResponse( + **document_schema.model_dump(), + transformation_job=job_info + ) + + return APIResponse.success_response(response) @router.delete( diff --git a/backend/app/core/doctransform/registry.py b/backend/app/core/doctransform/registry.py new file mode 100644 index 00000000..51f94944 --- /dev/null +++ b/backend/app/core/doctransform/registry.py @@ -0,0 +1,115 @@ +from pathlib import Path +from typing import Type, Dict, Set, Tuple, Optional + +from .transformer import Transformer +from .test_transformer import TestTransformer +from .zerox_transformer import ZeroxTransformer + +class TransformationError(Exception): + """Raised when a document transformation fails.""" + +# Map transformer names to their classes +TRANSFORMERS: Dict[str, Type[Transformer]] = { + "default": ZeroxTransformer, + "test": TestTransformer, + "zerox": ZeroxTransformer, +} + +# Define supported transformations: (source_format, target_format) -> [available_transformers] +SUPPORTED_TRANSFORMATIONS: Dict[Tuple[str, str], Dict[str, str]] = { + ("pdf", "markdown"): { + "default": "zerox", + "zerox": "zerox", + }, + # Future transformations can be added here + # ("docx", "markdown"): {"default": "pandoc", "pandoc": "pandoc"}, + # ("html", "markdown"): {"default": "pandoc", "pandoc": "pandoc"}, +} + +# Map file extensions to format names +EXTENSION_TO_FORMAT: Dict[str, str] = { + ".pdf": "pdf", + ".docx": "docx", + ".doc": "doc", + ".html": "html", + ".htm": "html", + ".txt": "text", + ".md": "markdown", + ".markdown": "markdown", +} + +# Map format names to file extensions +FORMAT_TO_EXTENSION: Dict[str, str] = { + "pdf": ".pdf", + "docx": ".docx", + "doc": ".doc", + "html": ".html", + "text": ".txt", + "markdown": ".md", +} + +def get_file_format(filename: str) -> str: + """Extract format from filename extension.""" + ext = Path(filename).suffix.lower() + format_name = EXTENSION_TO_FORMAT.get(ext) + if not format_name: + raise ValueError(f"Unsupported file extension: {ext}") + return format_name + +def get_supported_transformations() -> Dict[Tuple[str, str], Set[str]]: + """Get all supported transformation combinations.""" + return { + key: set(transformers.keys()) + for key, transformers in SUPPORTED_TRANSFORMATIONS.items() + } + +def is_transformation_supported(source_format: str, target_format: str) -> bool: + """Check if a transformation from source_format to target_format is supported.""" + return (source_format, target_format) in SUPPORTED_TRANSFORMATIONS + +def get_available_transformers(source_format: str, target_format: str) -> Dict[str, str]: + """Get available transformers for a specific transformation.""" + return SUPPORTED_TRANSFORMATIONS.get((source_format, target_format), {}) + +def resolve_transformer(source_format: str, target_format: str, transformer_name: Optional[str] = None) -> str: + """ + Resolve the actual transformer to use for a transformation. + Returns the transformer name to use. + """ + available_transformers = get_available_transformers(source_format, target_format) + + if not available_transformers: + raise ValueError( + f"Transformation from {source_format} to {target_format} is not supported" + ) + + if transformer_name is None: + transformer_name = "default" + + if transformer_name not in available_transformers: + available = ", ".join(available_transformers.keys()) + raise ValueError( + f"Transformer '{transformer_name}' not available for {source_format} to {target_format}. " + f"Available: {available}" + ) + + return available_transformers[transformer_name] + +def convert_document(input_path: Path, output_path: Path, transformer_name: str = "default") -> Path: + """ + Select and run the specified transformer on the input_path, writing to output_path. + Returns the path to the transformed file. + """ + try: + transformer_cls = TRANSFORMERS[transformer_name] + except KeyError: + available = ", ".join(TRANSFORMERS.keys()) + raise ValueError(f"Transformer '{transformer_name}' not found. Available: {available}") + + transformer = transformer_cls() + try: + return transformer.transform(input_path, output_path) + except Exception as e: + raise TransformationError( + f"Error applying transformer '{transformer_name}': {e}" + ) from e diff --git a/backend/app/core/doctransform/service.py b/backend/app/core/doctransform/service.py new file mode 100644 index 00000000..a217a3f4 --- /dev/null +++ b/backend/app/core/doctransform/service.py @@ -0,0 +1,128 @@ +import tempfile +import shutil +import logging +from pathlib import Path +from uuid import uuid4, UUID + +from fastapi import BackgroundTasks, UploadFile +from tenacity import retry, wait_exponential, stop_after_attempt +from sqlmodel import Session +from starlette.datastructures import Headers + +from app.crud.doc_transformation_job import DocTransformationJobCrud +from app.crud.document import DocumentCrud +from app.models.document import Document +from app.models.doc_transformation_job import TransformationStatus +from app.models import User +from app.core.cloud import get_cloud_storage +from app.api.deps import CurrentUserOrgProject +from app.core.doctransform.registry import convert_document, FORMAT_TO_EXTENSION +from app.core.db import engine + +logger = logging.getLogger(__name__) + +def start_job( + db: Session, + current_user: CurrentUserOrgProject, + source_document_id: UUID, + transformer_name: str, + target_format: str, + background_tasks: BackgroundTasks, +) -> UUID: + job_crud = DocTransformationJobCrud(session=db, project_id=current_user.project_id) + job = job_crud.create(source_document_id=source_document_id) + + # Extract the project ID before passing to background task + project_id = current_user.project_id + background_tasks.add_task(execute_job, project_id, job.id, transformer_name, target_format) + logger.info(f"[start_job] Job scheduled for document transformation | id: {job.id}, project_id: {project_id}") + return job.id + +@retry(wait=wait_exponential(multiplier=5, min=5, max=10), stop=stop_after_attempt(3)) +def execute_job( + project_id: int, + job_id: UUID, + transformer_name: str, + target_format: str, +): + try: + logger.info(f"[execute_job started] Transformation Job started | job_id={job_id} | transformer_name={transformer_name} | target_format={target_format} | project_id={project_id}") + + # Update job status to PROCESSING and fetch source document info + with Session(engine) as db: + job_crud = DocTransformationJobCrud(session=db, project_id=project_id) + job = job_crud.update_status(job_id, TransformationStatus.PROCESSING) + + doc_crud = DocumentCrud(session=db, project_id=project_id) + + source_doc = doc_crud.read_one(job.source_document_id) + + source_doc_id = source_doc.id + source_doc_fname = source_doc.fname + source_doc_object_store_url = source_doc.object_store_url + + storage = get_cloud_storage(session=db, project_id=project_id) + + # Download and transform document + body = storage.stream(source_doc_object_store_url) + tmp_dir = Path(tempfile.mkdtemp()) + tmp_in = tmp_dir / f"{source_doc_id}" + with open(tmp_in, "wb") as f: + shutil.copyfileobj(body, f) + + # prepare output file path + fname_no_ext = Path(source_doc_fname).stem + target_extension = FORMAT_TO_EXTENSION.get(target_format, f".{target_format}") + transformed_doc_id = uuid4() + tmp_out = tmp_dir / f"{fname_no_ext}{target_extension}" + + # transform document - now returns the output file path + convert_document(tmp_in, tmp_out, transformer_name) + + # Determine content type based on target format + content_type_map = { + "markdown": "text/markdown", + "text": "text/plain", + "html": "text/html", + } + content_type = content_type_map.get(target_format, "text/plain") + + + # upload transformed file and create document record + with open(tmp_out, "rb") as fobj: + file_upload = UploadFile( + filename=tmp_out.name, + file=fobj, + headers=Headers({"content-type": content_type}), + ) + dest = storage.put(file_upload, Path(str(transformed_doc_id))) + + # create new Document record + with Session(engine) as db: + new_doc = Document( + id=transformed_doc_id, + project_id=project_id, + fname=tmp_out.name, + object_store_url=str(dest), + source_document_id=source_doc_id, + ) + created = DocumentCrud(db, project_id).update(new_doc) + + job_crud = DocTransformationJobCrud(session=db, project_id=project_id) + job_crud.update_status(job_id, TransformationStatus.COMPLETED, transformed_document_id=created.id) + + logger.info(f"[execute_job] Doc Transformation job completed | job_id={job_id} | transformed_doc_id={created.id} | project_id={project_id}") + + except Exception as e: + logger.error(f"Transformation job failed | job_id={job_id} | error={e}", exc_info=True) + try: + with Session(engine) as db: + job_crud = DocTransformationJobCrud(session=db, project_id=project_id) + job_crud.update_status(job_id, TransformationStatus.FAILED, error_message=str(e)) + logger.info(f"[execute_job] Doc Transformation job failed | job_id={job_id} | error={e}") + except Exception as db_error: + logger.error(f"Failed to update job status to FAILED | job_id={job_id} | db_error={db_error}") + raise + finally: + if tmp_dir and tmp_dir.exists(): + shutil.rmtree(tmp_dir) diff --git a/backend/app/core/doctransform/test_transformer.py b/backend/app/core/doctransform/test_transformer.py new file mode 100644 index 00000000..66d5ba32 --- /dev/null +++ b/backend/app/core/doctransform/test_transformer.py @@ -0,0 +1,15 @@ +from pathlib import Path +from .transformer import Transformer + +class TestTransformer(Transformer): + """ + A test transformer that returns a hardcoded lorem ipsum string. + """ + + def transform(self, input_path: Path, output_path: Path) -> Path: + content = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + ) + output_path.write_text(content, encoding='utf-8') + return output_path diff --git a/backend/app/core/doctransform/transformer.py b/backend/app/core/doctransform/transformer.py new file mode 100644 index 00000000..f487aaed --- /dev/null +++ b/backend/app/core/doctransform/transformer.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod +from pathlib import Path + +class Transformer(ABC): + """Abstract base for document transformers.""" + + @abstractmethod + def transform(self, input_path: Path, output_path: Path) -> Path: + """ + Transform the document at input_path and write the result to output_path. + Returns the path to the transformed file. + """ + pass diff --git a/backend/app/core/doctransform/zerox_transformer.py b/backend/app/core/doctransform/zerox_transformer.py new file mode 100644 index 00000000..6a974cbd --- /dev/null +++ b/backend/app/core/doctransform/zerox_transformer.py @@ -0,0 +1,45 @@ +from asyncio import Runner +import logging +from pathlib import Path +from .transformer import Transformer +from pyzerox import zerox + +class ZeroxTransformer(Transformer): + """ + Transformer that uses zerox to extract content from PDFs. + """ + + def __init__(self, model: str = "gpt-4o"): + self.model = model + + def transform(self, input_path: Path, output_path: Path) -> Path: + logging.info(f"ZeroxTransformer Started: {input_path} (model={self.model})") + try: + with Runner() as runner: + result = runner.run(zerox( + file_path=str(input_path), + model=self.model, + )) + if result is None or not hasattr(result, "pages") or result.pages is None: + raise RuntimeError("Zerox returned no pages. This may indicate a PDF/image conversion failure (is Poppler installed and in PATH?)") + + with output_path.open("w", encoding="utf-8") as output_file: + for page in result.pages: + if not getattr(page, "content", None): + continue + output_file.write(page.content) + output_file.write("\n\n") + + logging.info(f"[ZeroxTransformer.transform] Transformation completed, output written to: {output_path}") + return output_path + except Exception as e: + logging.error( + f"ZeroxTransformer failed for {input_path}: {e}\n" + "This may be due to a missing Poppler installation or a corrupt PDF file.", + exc_info=True + ) + raise RuntimeError( + f"Failed to extract content from PDF. " + f"Check that Poppler is installed and in your PATH. Original error: {e}" + ) from e + diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index 35602c94..abe65c73 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -8,6 +8,7 @@ from .document import DocumentCrud from .document_collection import DocumentCollectionCrud +from .doc_transformation_job import DocTransformationJobCrud from .organization import ( create_organization, diff --git a/backend/app/crud/doc_transformation_job.py b/backend/app/crud/doc_transformation_job.py new file mode 100644 index 00000000..4a84cd43 --- /dev/null +++ b/backend/app/crud/doc_transformation_job.py @@ -0,0 +1,84 @@ +import logging +from uuid import UUID +from typing import List, Optional +from sqlmodel import Session, select, and_, join +from app.crud import DocumentCrud +from app.models import DocTransformationJob, TransformationStatus +from app.models.document import Document +from app.core.util import now +from app.core.exception_handlers import HTTPException + +logger = logging.getLogger(__name__) + +class DocTransformationJobCrud: + def __init__(self, session: Session, project_id: int): + self.session = session + self.project_id = project_id + + def create(self, source_document_id: UUID) -> DocTransformationJob: + # Ensure the source document exists and is not deleted + DocumentCrud(self.session, self.project_id).read_one(source_document_id) + + job = DocTransformationJob(source_document_id=source_document_id) + self.session.add(job) + self.session.commit() + self.session.refresh(job) + logger.info(f"[DocTransformationJobCrud.create] Created new transformation job | id: {job.id}, source_document_id: {source_document_id}") + return job + + def read_one(self, job_id: UUID) -> DocTransformationJob: + statement = ( + select(DocTransformationJob) + .join(Document, DocTransformationJob.source_document_id == Document.id) + .where( + and_( + DocTransformationJob.id == job_id, + Document.project_id == self.project_id, + Document.is_deleted.is_(False) + ) + ) + ) + + job = self.session.exec(statement).one_or_none() + if not job: + logger.warning(f"[DocTransformationJobCrud.read_one] Job not found or Document is deleted | id: {job_id}, project_id: {self.project_id}") + raise HTTPException(status_code=404, detail="Transformation job not found") + return job + + def read_each(self, job_ids: set[UUID]) -> list[DocTransformationJob]: + statement = ( + select(DocTransformationJob) + .join(Document, DocTransformationJob.source_document_id == Document.id) + .where( + and_( + DocTransformationJob.id.in_(list(job_ids)), + Document.project_id == self.project_id, + Document.is_deleted.is_(False) + ) + ) + ) + + jobs = self.session.exec(statement).all() + return jobs + + def update_status( + self, + job_id: UUID, + status: TransformationStatus, + *, + error_message: Optional[str] = None, + transformed_document_id: Optional[UUID] = None, + ) -> DocTransformationJob: + job = self.read_one(job_id) + job.status = status + job.updated_at = now() + if error_message is not None: + job.error_message = error_message + if transformed_document_id is not None: + job.transformed_document_id = transformed_document_id + + self.session.add(job) + self.session.commit() + self.session.refresh(job) + logger.info(f"[DocTransformationJobCrud.update_status] Updated job status | id: {job.id}, status: {status}") + return job diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e4a85a4e..82639861 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -2,7 +2,8 @@ from .auth import Token, TokenPayload from .collection import Collection -from .document import Document, DocumentPublic +from .document import Document, DocumentPublic, DocumentUploadResponse, TransformationJobInfo +from .doc_transformation_job import DocTransformationJob, DocTransformationJobs, TransformationStatus from .document_collection import DocumentCollection from .message import Message diff --git a/backend/app/models/doc_transformation_job.py b/backend/app/models/doc_transformation_job.py new file mode 100644 index 00000000..d7bd86ff --- /dev/null +++ b/backend/app/models/doc_transformation_job.py @@ -0,0 +1,30 @@ +import enum +from uuid import UUID, uuid4 +from typing import Optional +from datetime import datetime +from sqlmodel import SQLModel, Field +from app.core.util import now + + +class TransformationStatus(str, enum.Enum): + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class DocTransformationJob(SQLModel, table=True): + __tablename__ = "doc_transformation_job" + + id: UUID = Field(default_factory=uuid4, primary_key=True) + source_document_id: UUID = Field(foreign_key="document.id") + transformed_document_id: Optional[UUID] = Field(default=None, foreign_key="document.id") + status: TransformationStatus = Field(default=TransformationStatus.PENDING) + error_message: Optional[str] = Field(default=None) + created_at: datetime = Field(default_factory=now) + updated_at: datetime = Field(default_factory=now) + + +class DocTransformationJobs(SQLModel): + jobs: list[DocTransformationJob] + jobs_not_found: list[UUID] \ No newline at end of file diff --git a/backend/app/models/document.py b/backend/app/models/document.py index d900e8ad..2f44aeb0 100644 --- a/backend/app/models/document.py +++ b/backend/app/models/document.py @@ -1,5 +1,6 @@ from uuid import UUID, uuid4 from datetime import datetime +from typing import Optional from sqlmodel import Field, SQLModel @@ -32,6 +33,11 @@ class Document(DocumentBase, table=True): ) is_deleted: bool = Field(default=False) deleted_at: datetime | None + source_document_id: Optional[UUID] = Field( + default=None, + foreign_key="document.id", + nullable=True, + ) class DocumentPublic(DocumentBase): @@ -45,3 +51,37 @@ class DocumentPublic(DocumentBase): updated_at: datetime = Field( description="The timestamp when the document was last updated" ) + source_document_id: UUID | None = Field( + default=None, + description="The ID of the source document if this document is a transformation" + ) + signed_url: str | None = Field( + default=None, + description="A signed URL for accessing the document" + ) + + +class TransformationJobInfo(SQLModel): + message: str + job_id: UUID = Field( + description="The unique identifier of the transformation job" + ) + source_format: str = Field( + description="The format of the source document" + ) + target_format: str = Field( + description="The format of the target document" + ) + transformer: str = Field( + description="The name of the transformer used" + ) + status_check_url: str = Field( + description="The URL to check the status of the transformation job" + ) + + +class DocumentUploadResponse(DocumentPublic): + signed_url: str = Field( + description="A signed URL for accessing the document" + ) + transformation_job: TransformationJobInfo | None = None diff --git a/backend/app/tests/api/routes/documents/test_route_document_info.py b/backend/app/tests/api/routes/documents/test_route_document_info.py index 5ed92143..36c95493 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_info.py +++ b/backend/app/tests/api/routes/documents/test_route_document_info.py @@ -1,6 +1,7 @@ import pytest from sqlmodel import Session +from app.crud import get_project_by_id from app.tests.utils.document import ( DocumentComparator, DocumentMaker, diff --git a/backend/app/tests/api/routes/documents/test_route_document_list.py b/backend/app/tests/api/routes/documents/test_route_document_list.py index 8b2ec7a7..fc5b608c 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_list.py +++ b/backend/app/tests/api/routes/documents/test_route_document_list.py @@ -1,6 +1,7 @@ import pytest from sqlmodel import Session +from app.crud import get_project_by_id from app.tests.utils.document import ( DocumentComparator, DocumentStore, diff --git a/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py b/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py index 179d247d..46eccb23 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py +++ b/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py @@ -12,6 +12,7 @@ import openai_responses from openai_responses import OpenAIMock +from app.crud import get_project_by_id from app.core.cloud import AmazonCloudStorageClient from app.core.config import settings from app.models import Document diff --git a/backend/app/tests/api/routes/documents/test_route_document_remove.py b/backend/app/tests/api/routes/documents/test_route_document_remove.py index 7c1e3476..b7c7f15c 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_remove.py +++ b/backend/app/tests/api/routes/documents/test_route_document_remove.py @@ -1,10 +1,11 @@ import pytest import openai_responses from openai_responses import OpenAIMock -from openai import OpenAI +from openai import OpenAI, project from sqlmodel import Session, select from unittest.mock import patch +from app.crud import get_project_by_id from app.models import Document from app.tests.utils.document import ( DocumentMaker, diff --git a/backend/app/tests/api/routes/documents/test_route_document_upload.py b/backend/app/tests/api/routes/documents/test_route_document_upload.py index 25232861..b5592eef 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_upload.py +++ b/backend/app/tests/api/routes/documents/test_route_document_upload.py @@ -3,6 +3,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile from urllib.parse import urlparse +from unittest.mock import patch import pytest from moto import mock_aws @@ -21,16 +22,22 @@ class WebUploader(WebCrawler): - def put(self, route: Route, scratch: Path): + def put(self, route: Route, scratch: Path, target_format: str = None, transformer: str = None): (mtype, _) = mimetypes.guess_type(str(scratch)) - with scratch.open("rb") as fp: - return self.client.post( - str(route), - headers={"X-API-KEY": self.user_api_key.key}, - files={ - "src": (str(scratch), fp, mtype), - }, - ) + files = {"src": (str(scratch), scratch.open("rb"), mtype)} + + data = {} + if target_format: + data["target_format"] = target_format + if transformer: + data["transformer"] = transformer + + return self.client.post( + str(route), + headers={"X-API-KEY": self.user_api_key.key}, + files=files, + data=data, + ) @pytest.fixture @@ -40,6 +47,22 @@ def scratch(): yield Path(fp.name) +@pytest.fixture +def pdf_scratch(): + # Create a test PDF file for transformation tests + with NamedTemporaryFile(mode="w", suffix=".pdf", delete=False) as fp: + fp.write("%PDF-1.4\n1 0 obj<>endobj\n") + fp.write("2 0 obj<>endobj\n") + fp.write("3 0 obj<>endobj\n") + fp.write("xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n") + fp.write("0000000074 00000 n \n0000000120 00000 n \ntrailer<>\n") + fp.write("startxref\n202\n%%EOF") + fp.flush() + yield Path(fp.name) + # Clean up the temporary file + Path(fp.name).unlink() + + @pytest.fixture def route(): return Route("upload") @@ -101,3 +124,179 @@ def test_adds_to_S3( key = key.relative_to(key.root) assert aws.client.head_object(Bucket=url.netloc, Key=str(key)) + + def test_upload_without_transformation( + self, + db: Session, + route: Route, + scratch: Path, + uploader: WebUploader, + ): + """Test basic upload without any transformation parameters.""" + aws = AmazonCloudStorageClient() + aws.create() + + response = httpx_to_standard(uploader.put(route, scratch)) + + assert response.success is True + assert response.data["transformation_job"] is None + assert "id" in response.data + assert "fname" in response.data + + @patch('app.core.doctransform.service.start_job') + def test_upload_with_transformation( + self, + mock_start_job, + db: Session, + route: Route, + pdf_scratch: Path, + uploader: WebUploader, + ): + """Test upload with valid transformation parameters.""" + aws = AmazonCloudStorageClient() + aws.create() + + # Mock the background job creation + mock_job_id = "12345678-1234-5678-9abc-123456789012" + mock_start_job.return_value = mock_job_id + + response = httpx_to_standard(uploader.put(route, pdf_scratch, target_format="markdown")) + + assert response.success is True + assert response.data["transformation_job"] is not None + + transformation_job = response.data["transformation_job"] + assert transformation_job["job_id"] == mock_job_id + assert transformation_job["source_format"] == "pdf" + assert transformation_job["target_format"] == "markdown" + assert transformation_job["transformer"] == "zerox" # Default transformer for pdf->markdown + assert transformation_job["status_check_url"] == f"/documents/transformations/{mock_job_id}" + assert "message" in transformation_job + + @patch('app.core.doctransform.service.start_job') + def test_upload_with_specific_transformer( + self, + mock_start_job, + db: Session, + route: Route, + pdf_scratch: Path, + uploader: WebUploader, + ): + """Test upload with specific transformer specified.""" + aws = AmazonCloudStorageClient() + aws.create() + + mock_job_id = "12345678-1234-5678-9abc-123456789012" + mock_start_job.return_value = mock_job_id + + response = httpx_to_standard(uploader.put( + route, pdf_scratch, target_format="markdown", transformer="zerox" + )) + + assert response.success is True + transformation_job = response.data["transformation_job"] + assert transformation_job["transformer"] == "zerox" + + def test_upload_with_unsupported_transformation( + self, + db: Session, + route: Route, + scratch: Path, + uploader: WebUploader, + ): + """Test upload with unsupported transformation returns error.""" + aws = AmazonCloudStorageClient() + aws.create() + + response = uploader.put(route, scratch, target_format="pdf") + + assert response.status_code == 400 + error_data = response.json() + assert "Transformation from text to pdf is not supported" in error_data["error"] + + def test_upload_with_invalid_transformer( + self, + db: Session, + route: Route, + pdf_scratch: Path, + uploader: WebUploader, + ): + """Test upload with invalid transformer name returns error.""" + aws = AmazonCloudStorageClient() + aws.create() + + response = uploader.put( + route, pdf_scratch, target_format="markdown", transformer="invalid_transformer" + ) + + assert response.status_code == 400 + error_data = response.json() + assert "Transformer 'invalid_transformer' not available" in error_data["error"] + assert "Available transformers:" in error_data["error"] + + def test_upload_with_unsupported_file_extension( + self, + db: Session, + route: Route, + uploader: WebUploader, + ): + """Test upload with unsupported file extension returns error.""" + aws = AmazonCloudStorageClient() + aws.create() + + # Create a file with unsupported extension + with NamedTemporaryFile(mode="w", suffix=".xyz", delete=False) as fp: + fp.write("test content") + fp.flush() + unsupported_file = Path(fp.name) + + try: + response = uploader.put(route, unsupported_file, target_format="markdown") + assert response.status_code == 400 + error_data = response.json() + assert "Unsupported file extension: .xyz" in error_data["error"] + finally: + unsupported_file.unlink() + + @patch('app.core.doctransform.service.start_job') + def test_transformation_job_created_in_database( + self, + mock_start_job, + db: Session, + route: Route, + pdf_scratch: Path, + uploader: WebUploader, + ): + """Test that transformation job is properly stored in the database.""" + aws = AmazonCloudStorageClient() + aws.create() + + mock_job_id = "12345678-1234-5678-9abc-123456789012" + mock_start_job.return_value = mock_job_id + + response = httpx_to_standard(uploader.put(route, pdf_scratch, target_format="markdown")) + + mock_start_job.assert_called_once() + args, kwargs = mock_start_job.call_args + + # Check that start_job was called with the right arguments + assert 'transformer_name' in kwargs or len(args) >= 4 + + def test_upload_response_structure_without_transformation( + self, + db: Session, + route: Route, + scratch: Path, + uploader: WebUploader, + ): + """Test the response structure for upload without transformation.""" + aws = AmazonCloudStorageClient() + aws.create() + + response = httpx_to_standard(uploader.put(route, scratch)) + + required_fields = ["id", "project_id", "fname", "inserted_at", "updated_at", "source_document_id"] + for field in required_fields: + assert field in response.data + + assert response.data["transformation_job"] is None diff --git a/backend/app/tests/api/routes/test_doc_transformation_job.py b/backend/app/tests/api/routes/test_doc_transformation_job.py new file mode 100644 index 00000000..a0e4a7b4 --- /dev/null +++ b/backend/app/tests/api/routes/test_doc_transformation_job.py @@ -0,0 +1,357 @@ +import pytest +from uuid import UUID +from fastapi.testclient import TestClient +from sqlmodel import Session + +from app.core.config import settings +from app.crud.doc_transformation_job import DocTransformationJobCrud +from app.models import TransformationStatus +from app.tests.utils.document import DocumentStore +from app.tests.utils.utils import get_project +from app.models import APIKeyPublic +from app.crud.project import get_project_by_id + + + +class TestGetTransformationJob: + def test_get_existing_job_success( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test successfully retrieving an existing transformation job.""" + document = DocumentStore(db, user_api_key.project_id).put() + job = DocTransformationJobCrud(db, user_api_key.project_id) + created_job = job.create(document.id) + + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/{created_job.id}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert "data" in data + assert data["data"]["id"] is not None + assert data["data"]["source_document_id"] == str(document.id) + assert data["data"]["status"] == TransformationStatus.PENDING + assert data["data"]["error_message"] is None + assert data["data"]["transformed_document_id"] is None + + def test_get_nonexistent_job_404( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test getting a non-existent transformation job returns 404.""" + fake_uuid = "00000000-0000-0000-0000-000000000001" + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/{fake_uuid}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 404 + + def test_get_job_invalid_uuid_422( + self, + client: TestClient, + user_api_key: APIKeyPublic + ): + """Test getting a job with invalid UUID format returns 422.""" + invalid_uuid = "not-a-uuid" + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/{invalid_uuid}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 422 + + def test_get_job_different_project_404( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic, + superuser_api_key: APIKeyPublic + ): + """Test that jobs from different projects are not accessible.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + document = store.put() + job = crud.create(document.id) + + # Try to access with user from different project (superuser) + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/{job.id}", + headers={"X-API-KEY": superuser_api_key.key} + ) + + assert response.status_code == 404 + + def test_get_completed_job_with_result( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test getting a completed job with transformation result.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + source_document = store.put() + transformed_document = store.put() + job = crud.create(source_document.id) + + # Update job to completed status + crud.update_status( + job.id, + TransformationStatus.COMPLETED, + transformed_document_id=transformed_document.id + ) + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/{job.id}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert data["data"]["status"] == TransformationStatus.COMPLETED + assert data["data"]["transformed_document_id"] == str(transformed_document.id) + + def test_get_failed_job_with_error( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test getting a failed job with error message.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + document = store.put() + job = crud.create(document.id) + error_msg = "Transformation failed due to invalid format" + + # Update job to failed status + crud.update_status( + job.id, + TransformationStatus.FAILED, + error_message=error_msg + ) + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/{job.id}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert data["data"]["status"] == TransformationStatus.FAILED + assert data["data"]["error_message"] == error_msg + + +class TestGetMultipleTransformationJobs: + def test_get_multiple_jobs_success( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test successfully retrieving multiple transformation jobs.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + documents = store.fill(3) + jobs = [crud.create(doc.id) for doc in documents] + job_ids_str = ",".join(str(job.id) for job in jobs) + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert "data" in data + assert len(data["data"]["jobs"]) == 3 + assert len(data["data"]["jobs_not_found"]) == 0 + + returned_ids = {job["id"] for job in data["data"]["jobs"]} + expected_ids = {str(job.id) for job in jobs} + assert returned_ids == expected_ids + + def test_get_mixed_existing_nonexisting_jobs( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test retrieving a mix of existing and non-existing jobs.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + documents = store.fill(2) + jobs = [crud.create(doc.id) for doc in documents] + fake_uuid = "00000000-0000-0000-0000-000000000001" + + job_ids_str = f"{jobs[0].id},{jobs[1].id},{fake_uuid}" + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["jobs"]) == 2 + assert len(data["data"]["jobs_not_found"]) == 1 + assert data["data"]["jobs_not_found"][0] == fake_uuid + + def test_get_jobs_with_empty_string( + self, + client: TestClient, + user_api_key: APIKeyPublic + ): + """Test retrieving jobs with empty job_ids parameter.""" + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids=", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["jobs"]) == 0 + assert len(data["data"]["jobs_not_found"]) == 0 + + def test_get_jobs_with_whitespace_only( + self, + client: TestClient, + user_api_key: APIKeyPublic + ): + """Test retrieving jobs with whitespace-only job_ids.""" + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids= , , ", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["jobs"]) == 0 + assert len(data["data"]["jobs_not_found"]) == 0 + + def test_get_jobs_invalid_uuid_format_422( + self, + client: TestClient, + user_api_key: APIKeyPublic + ): + """Test that invalid UUID format returns 422.""" + invalid_uuids = "not-a-uuid,also-not-uuid" + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids={invalid_uuids}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 422 + data = response.json() + assert "Invalid UUID(s) provided" in data["error"] + + def test_get_jobs_mixed_valid_invalid_uuid_422( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test that mixed valid/invalid UUIDs returns 422.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + document = store.put() + job = crud.create(document.id) + + job_ids_str = f"{job.id},not-a-uuid" + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 422 + data = response.json() + assert "Invalid UUID(s) provided" in data["error"] + assert "not-a-uuid" in data["error"] + + def test_get_jobs_missing_parameter_422( + self, + client: TestClient, + user_api_key: APIKeyPublic + ): + """Test that missing job_ids parameter returns 422.""" + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 422 + + def test_get_jobs_different_project_not_found( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic, + superuser_api_key: APIKeyPublic + ): + """Test that jobs from different projects are not returned.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + document = store.put() + job = crud.create(document.id) + + # Try to access with user from different project (superuser) + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids={job.id}", + headers={"X-API-KEY": superuser_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["jobs"]) == 0 + assert len(data["data"]["jobs_not_found"]) == 1 + assert data["data"]["jobs_not_found"][0] == str(job.id) + + def test_get_jobs_with_various_statuses( + self, + client: TestClient, + db: Session, + user_api_key: APIKeyPublic + ): + """Test retrieving jobs with different statuses.""" + store = DocumentStore(db, user_api_key.project_id) + crud = DocTransformationJobCrud(db, user_api_key.project_id) + documents = store.fill(4) + jobs = [crud.create(doc.id) for doc in documents] + + crud.update_status(jobs[1].id, TransformationStatus.PROCESSING) + crud.update_status(jobs[2].id, TransformationStatus.COMPLETED, transformed_document_id=documents[2].id) + crud.update_status(jobs[3].id, TransformationStatus.FAILED, error_message="Test error") + + job_ids_str = ",".join(str(job.id) for job in jobs) + + response = client.get( + f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + headers={"X-API-KEY": user_api_key.key} + ) + + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["jobs"]) == 4 + + # Check that all statuses are represented + statuses = {job["status"] for job in data["data"]["jobs"]} + expected_statuses = { + TransformationStatus.PENDING, + TransformationStatus.PROCESSING, + TransformationStatus.COMPLETED, + TransformationStatus.FAILED + } + assert statuses == expected_statuses diff --git a/backend/app/tests/core/doctransformer/test_service/base.py b/backend/app/tests/core/doctransformer/test_service/base.py new file mode 100644 index 00000000..f6e14904 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_service/base.py @@ -0,0 +1,128 @@ +""" +Test organization and base classes for DocTransform service tests. + +This module contains: +- DocTransformTestBase: Base test class with common setup +- TestDataProvider: Common test data and configurations +- MockHelpers: Utilities for creating mocks and test fixtures + +All fixtures are automatically available from conftest.py in the same directory. +Test files can import these base classes and use fixtures without additional imports. +""" +from pathlib import Path +from typing import List +from urllib.parse import urlparse + +from app.core.cloud import AmazonCloudStorageClient +from app.core.config import settings +from app.models import Document, Project + + +class DocTransformTestBase: + """Base class for document transformation tests with common setup and utilities.""" + + def setup_aws_s3(self) -> AmazonCloudStorageClient: + """Setup AWS S3 for testing.""" + aws = AmazonCloudStorageClient() + aws.create() + return aws + + def create_s3_document_content( + self, + aws: AmazonCloudStorageClient, + project: Project, + document: Document, + content: bytes = b"Test document content" + ) -> bytes: + """Create content in S3 for a document.""" + parsed_url = urlparse(document.object_store_url) + s3_key = parsed_url.path.lstrip('/') + + aws.client.put_object( + Bucket=settings.AWS_S3_BUCKET, + Key=s3_key, + Body=content + ) + return content + + def verify_s3_content( + self, + aws: AmazonCloudStorageClient, + project: Project, + transformed_doc: Document, + expected_content: str = None + ) -> None: + """Verify the content stored in S3.""" + if expected_content is None: + expected_content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + + parsed_url = urlparse(transformed_doc.object_store_url) + + transformed_key = parsed_url.path.lstrip('/') + + response = aws.client.get_object( + Bucket=settings.AWS_S3_BUCKET, + Key=transformed_key + ) + transformed_content = response['Body'].read().decode('utf-8') + assert transformed_content == expected_content + + +class TestDataProvider: + """Provides test data and configurations for document transformation tests.""" + + @staticmethod + def get_format_test_cases() -> List[tuple]: + """Get test cases for different document formats.""" + return [ + ("markdown", ".md"), + ("text", ".txt"), + ("html", ".html"), + ] + + @staticmethod + def get_content_type_test_cases() -> List[tuple]: + """Get test cases for content types and extensions.""" + return [ + ("markdown", "text/markdown", ".md"), + ("text", "text/plain", ".txt"), + ("html", "text/html", ".html"), + ("unknown", "text/plain", ".unknown") # Default fallback + ] + + @staticmethod + def get_test_transformer_names() -> List[str]: + """Get list of test transformer names.""" + return ["test"] + + @staticmethod + def get_sample_document_content() -> bytes: + """Get sample document content for testing.""" + return b"This is a test document for transformation." + + +class MockHelpers: + """Helper methods for creating mocks in tests.""" + + @staticmethod + def create_failing_convert_document(fail_count: int = 1): + """Create a side effect function that fails specified times then succeeds.""" + call_count = 0 + def failing_convert_document(*args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count <= fail_count: + raise Exception("Transient error") + output_path = args[1] if len(args) > 1 else kwargs.get('output_path') + if output_path: + output_path.write_text("Success after retries", encoding='utf-8') + return output_path + raise ValueError("output_path is required") + return failing_convert_document + + @staticmethod + def create_persistent_failing_convert_document(error_message: str = "Persistent error"): + """Create a side effect function that always fails.""" + def persistent_failing_convert_document(*args, **kwargs): + raise Exception(error_message) + return persistent_failing_convert_document diff --git a/backend/app/tests/core/doctransformer/test_service/conftest.py b/backend/app/tests/core/doctransformer/test_service/conftest.py new file mode 100644 index 00000000..d69e8d11 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_service/conftest.py @@ -0,0 +1,71 @@ +""" +Pytest fixtures for document transformation service tests. +""" +import os +from typing import Any, Callable, Generator, Tuple +from unittest.mock import patch +from uuid import UUID + +import pytest +from fastapi import BackgroundTasks +from sqlmodel import Session +from tenacity import retry, stop_after_attempt, wait_fixed + +from app.crud import get_project_by_id +from app.models import User +from app.core.config import settings +from app.models import Document, Project, UserProjectOrg +from app.tests.utils.document import DocumentStore +from app.tests.utils.test_data import create_test_api_key + + +@pytest.fixture(scope="class") +def aws_credentials() -> None: + """Set up AWS credentials for moto.""" + os.environ["AWS_ACCESS_KEY_ID"] = "testing" + os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" + os.environ["AWS_SECURITY_TOKEN"] = "testing" + os.environ["AWS_SESSION_TOKEN"] = "testing" + os.environ["AWS_DEFAULT_REGION"] = settings.AWS_DEFAULT_REGION + + +@pytest.fixture +def fast_execute_job() -> Generator[Callable[[int, UUID, str, str], Any], None, None]: + """Create a version of execute_job without retry delays for faster testing.""" + from app.core.doctransform import service + + original_execute_job = service.execute_job + + @retry(stop=stop_after_attempt(2), wait=wait_fixed(0.01)) # Very fast retry for tests + def fast_execute_job_func(project_id: int, job_id: UUID, transformer_name: str, target_format: str) -> Any: + # Call the original function's implementation without the decorator + return original_execute_job.__wrapped__(project_id, job_id, transformer_name, target_format) + + with patch.object(service, 'execute_job', fast_execute_job_func): + yield fast_execute_job_func + + +@pytest.fixture +def current_user(db: Session) -> UserProjectOrg: + """Create a test user for testing.""" + api_key = create_test_api_key(db) + user = db.get(User, api_key.user_id) + return UserProjectOrg( + **user.model_dump(), + project_id=api_key.project_id, + organization_id=api_key.organization_id + ) + + +@pytest.fixture +def background_tasks() -> BackgroundTasks: + """Create BackgroundTasks instance.""" + return BackgroundTasks() + + +@pytest.fixture +def test_document(db: Session, current_user: UserProjectOrg) -> Tuple[Document, Project]: + """Create a test document for the current user's project.""" + store = DocumentStore(db, current_user.project_id) + project = get_project_by_id(session=db, project_id=current_user.project_id) + return store.put(), project diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py new file mode 100644 index 00000000..b3b7e2f4 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py @@ -0,0 +1,250 @@ +""" +Tests for the execute_job function in document transformation service. +""" +from typing import Any, Callable, Tuple +from unittest.mock import patch +from uuid import uuid4, UUID + +import pytest +from moto import mock_aws +from sqlmodel import Session +from tenacity import RetryError + +from app.crud import DocTransformationJobCrud, DocumentCrud +from app.core.doctransform.registry import TransformationError +from app.core.doctransform.service import execute_job +from app.core.exception_handlers import HTTPException +from app.models import Document, DocTransformationJob, Project, TransformationStatus +from app.tests.core.doctransformer.test_service.base import DocTransformTestBase, TestDataProvider + + +class TestExecuteJob(DocTransformTestBase): + """Test cases for the execute_job function.""" + + @pytest.mark.parametrize( + "target_format, expected_extension", + TestDataProvider.get_format_test_cases(), + ) + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_success( + self, + db: Session, + test_document: Tuple[Document, Project], + target_format: str, + expected_extension: str, + ) -> None: + """Test successful document transformation with multiple formats.""" + document, project = test_document + aws = self.setup_aws_s3() + + source_content = TestDataProvider.get_sample_document_content() + self.create_s3_document_content(aws, project, document, source_content) + + # Create transformation job + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + # Mock the Session to use our existing database session + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format=target_format + ) + + # Verify job completion + db.refresh(job) + assert job.status == TransformationStatus.COMPLETED + assert job.transformed_document_id is not None + assert job.error_message is None + + # Verify transformed document + document_crud = DocumentCrud(session=db, project_id=project.id) + transformed_doc = document_crud.read_one(job.transformed_document_id) + assert transformed_doc is not None + assert transformed_doc.fname.endswith(expected_extension) + assert "" in transformed_doc.fname + assert transformed_doc.source_document_id == document.id + assert transformed_doc.object_store_url is not None + + # Verify transformed content in S3 + self.verify_s3_content(aws, project, transformed_doc) + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_with_nonexistent_job( + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, UUID, str, str], Any] + ) -> None: + """Test job execution with non-existent job ID.""" + _, project = test_document + self.setup_aws_s3() + nonexistent_job_id = uuid4() + + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + # Execute job should fail because job doesn't exist + with pytest.raises((HTTPException, RetryError)): + fast_execute_job( + project_id=project.id, + job_id=nonexistent_job_id, + transformer_name="test", + target_format="markdown" + ) + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_with_missing_source_document( + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, UUID, str, str], Any] + ) -> None: + """Test job execution when source document is missing from S3.""" + document, project = test_document + self.setup_aws_s3() + + # Create job but don't upload document to S3 + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + with pytest.raises(Exception): + fast_execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify job was marked as failed + db.refresh(job) + assert job.status == TransformationStatus.FAILED + assert job.error_message is not None + assert job.transformed_document_id is None + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_with_transformer_error( + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, UUID, str, str], Any] + ) -> None: + """Test job execution when transformer raises an error.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + # Mock convert_document to raise TransformationError + with patch('app.core.doctransform.service.Session') as mock_session_class, \ + patch('app.core.doctransform.service.convert_document') as mock_convert: + + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + mock_convert.side_effect = TransformationError("Mock transformation error") + + # Due to retry mechanism, it will raise RetryError after exhausting retries + with pytest.raises((TransformationError, RetryError)): + fast_execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify job was marked as failed + db.refresh(job) + assert job.status == TransformationStatus.FAILED + assert "Mock transformation error" in job.error_message + assert job.transformed_document_id is None + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_status_transitions( + self, + db: Session, + test_document: Tuple[Document, Project] + ) -> None: + """Test that job status transitions correctly during execution.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + initial_status = job.status + db.commit() + + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify status progression by checking final job state + db.refresh(job) + assert job.status == TransformationStatus.COMPLETED + assert initial_status == TransformationStatus.PENDING + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_with_different_content_types( + self, + db: Session, + test_document: Tuple[Document, Project] + ) -> None: + """Test job execution produces correct content types for different formats.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + format_extensions = TestDataProvider.get_content_type_test_cases() + + for target_format, expected_content_type, expected_extension in format_extensions: + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format=target_format + ) + + # Verify transformation completed and check file extension + db.refresh(job) + assert job.status == TransformationStatus.COMPLETED + document_crud = DocumentCrud(session=db, project_id=project.id) + assert job.transformed_document_id is not None + transformed_doc = document_crud.read_one(job.transformed_document_id) + assert transformed_doc is not None + assert transformed_doc.fname.endswith(expected_extension) diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py new file mode 100644 index 00000000..80343865 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py @@ -0,0 +1,178 @@ +""" +Tests for retry mechanisms and error handling in document transformation service. +""" +from io import BytesIO +from typing import Any, Callable, Tuple +from unittest.mock import patch + +import pytest +from moto import mock_aws +from sqlmodel import Session +from tenacity import RetryError + +from app.crud import DocTransformationJobCrud +from app.core.doctransform.service import execute_job +from app.models import Document, Project, TransformationStatus +from app.tests.core.doctransformer.test_service.base import DocTransformTestBase, MockHelpers + + +class TestExecuteJobRetryAndErrors(DocTransformTestBase): + """Test cases for retry mechanisms and error handling in execute_job function.""" + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_with_storage_error( + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any] + ) -> None: + """Test job execution when S3 upload fails.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + # Mock storage.put to raise an error + with patch('app.core.doctransform.service.Session') as mock_session_class, \ + patch('app.core.doctransform.service.get_cloud_storage') as mock_storage_class: + + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + mock_storage = mock_storage_class.return_value + mock_storage.stream.return_value = BytesIO(b"test content") + mock_storage.put.side_effect = Exception("S3 upload failed") + + with pytest.raises(Exception): + fast_execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify job was marked as failed + db.refresh(job) + assert job.status == TransformationStatus.FAILED + assert "S3 upload failed" in job.error_message + assert job.transformed_document_id is None + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_retry_mechanism( + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any] + ) -> None: + """Test that retry mechanism works for transient failures.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + # Create a side effect that fails once then succeeds (fast retry will only try 2 times) + failing_convert_document = MockHelpers.create_failing_convert_document(fail_count=1) + + with patch('app.core.doctransform.service.Session') as mock_session_class, \ + patch('app.core.doctransform.service.convert_document', side_effect=failing_convert_document): + + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + fast_execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify the function was retried and eventually succeeded + db.refresh(job) + assert job.status == TransformationStatus.COMPLETED + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_exhausted_retries( + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any] + ) -> None: + """Test behavior when all retry attempts are exhausted.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + # Mock convert_document to always fail + persistent_failing_convert_document = MockHelpers.create_persistent_failing_convert_document("Persistent error") + + with patch('app.core.doctransform.service.Session') as mock_session_class, \ + patch('app.core.doctransform.service.convert_document', side_effect=persistent_failing_convert_document): + + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + with pytest.raises(Exception): + fast_execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify job was marked as failed after retries + db.refresh(job) + assert job.status == TransformationStatus.FAILED + assert "Persistent error" in job.error_message + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_database_error_during_completion( + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any] + ) -> None: + """Test handling of database errors when updating job completion.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + job = job_crud.create(source_document_id=document.id) + db.commit() + + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + # Mock DocumentCrud.update to fail when creating the transformed document + with patch('app.core.doctransform.service.DocumentCrud') as mock_doc_crud_class: + mock_doc_crud_instance = mock_doc_crud_class.return_value + mock_doc_crud_instance.read_one.return_value = document # Return valid document for source + mock_doc_crud_instance.update.side_effect = Exception("Database error during document creation") + + with pytest.raises(Exception): + fast_execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify job was marked as failed + db.refresh(job) + assert job.status == TransformationStatus.FAILED + assert "Database error during document creation" in job.error_message diff --git a/backend/app/tests/core/doctransformer/test_service/test_integration.py b/backend/app/tests/core/doctransformer/test_service/test_integration.py new file mode 100644 index 00000000..4c912b32 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_service/test_integration.py @@ -0,0 +1,167 @@ +""" +Integration tests for document transformation service. +""" +from typing import Tuple +from unittest.mock import patch + +import pytest +from fastapi import BackgroundTasks +from moto import mock_aws +from sqlmodel import Session + +from app.crud import DocTransformationJobCrud, DocumentCrud +from app.core.doctransform.service import execute_job, start_job +from app.models import Document, DocTransformationJob, Project, TransformationStatus, UserProjectOrg +from app.tests.core.doctransformer.test_service.base import DocTransformTestBase + + +class TestExecuteJobIntegration(DocTransformTestBase): + """Integration tests for execute_job function with minimal mocking.""" + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_end_to_end_workflow( + self, + db: Session, + test_document: Tuple[Document, Project] + ) -> None: + """Test complete end-to-end workflow from start_job to execute_job.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + # Start job using the service + current_user = UserProjectOrg( + id=1, + email="test@example.com", + project_id=project.id, + organization_id=project.organization_id + ) + background_tasks = BackgroundTasks() + + job_id = start_job( + db=db, + current_user=current_user, + source_document_id=document.id, + transformer_name="test", + target_format="markdown", + background_tasks=background_tasks, + ) + + # Verify job was created + job = db.get(DocTransformationJob, job_id) + assert job.status == TransformationStatus.PENDING + + # Execute the job manually (simulating background execution) + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify complete workflow + db.refresh(job) + assert job.status == TransformationStatus.COMPLETED + assert job.transformed_document_id is not None + + # Verify transformed document exists and is valid + document_crud = DocumentCrud(session=db, project_id=project.id) + transformed_doc = document_crud.read_one(job.transformed_document_id) + assert transformed_doc.source_document_id == document.id + assert "" in transformed_doc.fname + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_execute_job_concurrent_jobs( + self, + db: Session, + test_document: Tuple[Document, Project] + ) -> None: + """Test multiple concurrent job executions don't interfere with each other.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + # Create multiple jobs + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + jobs = [] + for i in range(3): + job = job_crud.create(source_document_id=document.id) + jobs.append(job) + db.commit() + + # Execute all jobs + for job in jobs: + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format="markdown" + ) + + # Verify all jobs completed successfully + for job in jobs: + db.refresh(job) + assert job.status == TransformationStatus.COMPLETED + assert job.transformed_document_id is not None + + @mock_aws + @pytest.mark.usefixtures("aws_credentials") + def test_multiple_format_transformations( + self, + db: Session, + test_document: Tuple[Document, Project] + ) -> None: + """Test transforming the same document to multiple formats.""" + document, project = test_document + aws = self.setup_aws_s3() + self.create_s3_document_content(aws, project, document) + + formats = ["markdown", "text", "html"] + jobs = [] + + # Create jobs for different formats + job_crud = DocTransformationJobCrud(session=db, project_id=project.id) + for target_format in formats: + job = job_crud.create(source_document_id=document.id) + jobs.append((job, target_format)) + db.commit() + + # Execute all jobs + for job, target_format in jobs: + with patch('app.core.doctransform.service.Session') as mock_session_class: + mock_session_class.return_value.__enter__.return_value = db + mock_session_class.return_value.__exit__.return_value = None + + execute_job( + project_id=project.id, + job_id=job.id, + transformer_name="test", + target_format=target_format + ) + + # Verify all jobs completed successfully with correct formats + document_crud = DocumentCrud(session=db, project_id=project.id) + for i, (job, target_format) in enumerate(jobs): + db.refresh(job) + assert job.status == TransformationStatus.COMPLETED + assert job.transformed_document_id is not None + + transformed_doc = document_crud.read_one(job.transformed_document_id) + assert transformed_doc is not None + # Verify correct file extension based on format + if target_format == "markdown": + assert transformed_doc.fname.endswith(".md") + elif target_format == "text": + assert transformed_doc.fname.endswith(".txt") + elif target_format == "html": + assert transformed_doc.fname.endswith(".html") diff --git a/backend/app/tests/core/doctransformer/test_service/test_start_job.py b/backend/app/tests/core/doctransformer/test_service/test_start_job.py new file mode 100644 index 00000000..62274825 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_service/test_start_job.py @@ -0,0 +1,156 @@ +""" +Tests for the start_job function in document transformation service. +""" +from typing import Any, Tuple +from uuid import uuid4 + +import pytest +from fastapi import BackgroundTasks +from sqlmodel import Session + +from app.core.doctransform.service import execute_job, start_job +from app.core.exception_handlers import HTTPException +from app.models import Document, DocTransformationJob, Project, TransformationStatus, UserProjectOrg +from app.tests.core.doctransformer.test_service.base import DocTransformTestBase, TestDataProvider + + +class TestStartJob(DocTransformTestBase): + """Test cases for the start_job function.""" + + def test_start_job_success( + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], + background_tasks: BackgroundTasks + ) -> None: + """Test successful job creation and scheduling.""" + document, _ = test_document + job_id = start_job( + db=db, + current_user=current_user, + source_document_id=document.id, + transformer_name="test-transformer", + target_format="markdown", + background_tasks=background_tasks, + ) + + job = db.get(DocTransformationJob, job_id) + assert job is not None + assert job.source_document_id == document.id + assert job.status == TransformationStatus.PENDING + assert job.error_message is None + assert job.transformed_document_id is None + + assert len(background_tasks.tasks) == 1 + task = background_tasks.tasks[0] + assert task.func == execute_job + assert task.args[0] == current_user.project_id + assert task.args[1] == job_id + assert task.args[2] == "test-transformer" + assert task.args[3] == "markdown" + + def test_start_job_with_nonexistent_document( + self, + db: Session, + current_user: UserProjectOrg, + background_tasks: BackgroundTasks + ) -> None: + """Test job creation with non-existent document raises error.""" + nonexistent_id = uuid4() + + with pytest.raises(HTTPException) as exc_info: + start_job( + db=db, + current_user=current_user, + source_document_id=nonexistent_id, + transformer_name="test-transformer", + target_format="markdown", + background_tasks=background_tasks, + ) + + assert exc_info.value.status_code == 404 + assert "Document not found" in str(exc_info.value.detail) + + def test_start_job_with_deleted_document( + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], + background_tasks: BackgroundTasks + ) -> None: + """Test job creation with deleted document raises error.""" + document, _ = test_document + + document.is_deleted = True + db.add(document) + db.commit() + + with pytest.raises(HTTPException) as exc_info: + start_job( + db=db, + current_user=current_user, + source_document_id=document.id, + transformer_name="test-transformer", + target_format="markdown", + background_tasks=background_tasks, + ) + + assert exc_info.value.status_code == 404 + assert "Document not found" in str(exc_info.value.detail) + + def test_start_job_with_different_formats( + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], + background_tasks: BackgroundTasks + ) -> None: + """Test job creation with different target formats.""" + document, _ = test_document + formats = ["markdown", "text", "html"] + + for target_format in formats: + job_id = start_job( + db=db, + current_user=current_user, + source_document_id=document.id, + transformer_name="test", + target_format=target_format, + background_tasks=background_tasks, + ) + + job = db.get(DocTransformationJob, job_id) + assert job is not None + assert job.status == TransformationStatus.PENDING + + task = background_tasks.tasks[-1] + assert task.args[3] == target_format + + @pytest.mark.parametrize("transformer_name", TestDataProvider.get_test_transformer_names()) + def test_start_job_with_different_transformers( + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], + background_tasks: BackgroundTasks, + transformer_name: str + ) -> None: + """Test job creation with different transformer names.""" + document, _ = test_document + + job_id = start_job( + db=db, + current_user=current_user, + source_document_id=document.id, + transformer_name=transformer_name, + target_format="markdown", + background_tasks=background_tasks, + ) + + job = db.get(DocTransformationJob, job_id) + assert job is not None + assert job.status == TransformationStatus.PENDING + + task = background_tasks.tasks[-1] + assert task.args[2] == transformer_name diff --git a/backend/app/tests/crud/collections/test_crud_collection_create.py b/backend/app/tests/crud/collections/test_crud_collection_create.py index 53293d28..bfd54d53 100644 --- a/backend/app/tests/crud/collections/test_crud_collection_create.py +++ b/backend/app/tests/crud/collections/test_crud_collection_create.py @@ -1,7 +1,7 @@ import openai_responses from sqlmodel import Session, select -from app.crud import CollectionCrud +from app.crud import CollectionCrud, get_project_by_id from app.models import DocumentCollection from app.tests.utils.document import DocumentStore from app.tests.utils.collection import get_collection diff --git a/backend/app/tests/crud/collections/test_crud_collection_read_all.py b/backend/app/tests/crud/collections/test_crud_collection_read_all.py index f8cc82fb..860541e0 100644 --- a/backend/app/tests/crud/collections/test_crud_collection_read_all.py +++ b/backend/app/tests/crud/collections/test_crud_collection_read_all.py @@ -3,7 +3,7 @@ from openai import OpenAI from sqlmodel import Session -from app.crud import CollectionCrud +from app.crud import CollectionCrud, get_project_by_id from app.models import Collection from app.tests.utils.document import DocumentStore from app.tests.utils.collection import get_collection diff --git a/backend/app/tests/crud/test_doc_transformation_job.py b/backend/app/tests/crud/test_doc_transformation_job.py new file mode 100644 index 00000000..602e0fd6 --- /dev/null +++ b/backend/app/tests/crud/test_doc_transformation_job.py @@ -0,0 +1,230 @@ +import pytest +from uuid import UUID +from sqlmodel import Session +from app.crud.doc_transformation_job import DocTransformationJobCrud +from app.models import DocTransformationJob, TransformationStatus +from app.core.exception_handlers import HTTPException +from app.tests.utils.document import DocumentStore +from app.tests.utils.utils import get_project, SequentialUuidGenerator + + +@pytest.fixture +def store(db: Session): + project = get_project(db) + return DocumentStore(db, project.id) + + +@pytest.fixture +def crud(db: Session, store: DocumentStore): + return DocTransformationJobCrud(db, store.project.id) + + +class TestDocTransformationJobCrudCreate: + def test_can_create_job_with_valid_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test creating a transformation job with a valid source document.""" + document = store.put() + + job = crud.create(document.id) + + assert job.id is not None + assert job.source_document_id == document.id + assert job.status == TransformationStatus.PENDING + assert job.error_message is None + assert job.transformed_document_id is None + assert job.created_at is not None + assert job.updated_at is not None + + def test_cannot_create_job_with_invalid_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test that creating a job with non-existent document raises an error.""" + invalid_id = next(SequentialUuidGenerator()) + + with pytest.raises(HTTPException) as exc_info: + crud.create(invalid_id) + + assert exc_info.value.status_code == 404 + assert "Document not found" in str(exc_info.value.detail) + + def test_cannot_create_job_with_deleted_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test that creating a job with a deleted document raises an error.""" + document = store.put() + # Mark document as deleted + document.is_deleted = True + db.add(document) + db.commit() + + with pytest.raises(HTTPException) as exc_info: + crud.create(document.id) + + assert exc_info.value.status_code == 404 + assert "Document not found" in str(exc_info.value.detail) + + +class TestDocTransformationJobCrudReadOne: + def test_can_read_existing_job(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test reading an existing transformation job.""" + document = store.put() + job = crud.create(document.id) + + result = crud.read_one(job.id) + + assert result.id == job.id + assert result.source_document_id == document.id + assert result.status == TransformationStatus.PENDING + + def test_cannot_read_nonexistent_job(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test that reading a non-existent job raises an error.""" + invalid_id = next(SequentialUuidGenerator()) + + with pytest.raises(HTTPException) as exc_info: + crud.read_one(invalid_id) + + assert exc_info.value.status_code == 404 + assert "Transformation job not found" in str(exc_info.value.detail) + + def test_cannot_read_job_with_deleted_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test that reading a job whose source document is deleted raises an error.""" + document = store.put() + job = crud.create(document.id) + + # Mark document as deleted + document.is_deleted = True + db.add(document) + db.commit() + + with pytest.raises(HTTPException) as exc_info: + crud.read_one(job.id) + + assert exc_info.value.status_code == 404 + assert "Transformation job not found" in str(exc_info.value.detail) + + def test_cannot_read_job_from_different_project(self, db: Session, store: DocumentStore): + """Test that reading a job from a different project raises an error.""" + document = store.put() + job_crud = DocTransformationJobCrud(db, store.project.id) + job = job_crud.create(document.id) + + # Try to read from different project + other_project = get_project(db, name="Dalgo") + other_crud = DocTransformationJobCrud(db, other_project.id) + + with pytest.raises(HTTPException) as exc_info: + other_crud.read_one(job.id) + + assert exc_info.value.status_code == 404 + assert "Transformation job not found" in str(exc_info.value.detail) + + +class TestDocTransformationJobCrudReadEach: + def test_can_read_multiple_existing_jobs(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test reading multiple existing transformation jobs.""" + documents = store.fill(3) + jobs = [crud.create(doc.id) for doc in documents] + job_ids = {job.id for job in jobs} + + results = crud.read_each(job_ids) + + assert len(results) == 3 + result_ids = {job.id for job in results} + assert result_ids == job_ids + + def test_read_partial_existing_jobs(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test reading a mix of existing and non-existing jobs.""" + documents = store.fill(2) + jobs = [crud.create(doc.id) for doc in documents] + job_ids = {job.id for job in jobs} + job_ids.add(next(SequentialUuidGenerator())) # Add non-existent ID + + results = crud.read_each(job_ids) + + assert len(results) == 2 # Only existing jobs returned + result_ids = {job.id for job in results} + assert result_ids == {job.id for job in jobs} + + def test_read_empty_job_set(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test reading an empty set of job IDs.""" + results = crud.read_each(set()) + + assert len(results) == 0 + + def test_cannot_read_jobs_from_different_project(self, db: Session, store: DocumentStore): + """Test that jobs from different projects are not returned.""" + document = store.put() + job_crud = DocTransformationJobCrud(db, store.project.id) + job = job_crud.create(document.id) + + # Try to read from different project + other_project = get_project(db, name="Dalgo") + other_crud = DocTransformationJobCrud(db, other_project.id) + + results = other_crud.read_each({job.id}) + + assert len(results) == 0 + + +class TestDocTransformationJobCrudUpdateStatus: + def test_can_update_status_to_processing(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test updating job status to processing.""" + document = store.put() + job = crud.create(document.id) + + updated_job = crud.update_status(job.id, TransformationStatus.PROCESSING) + + assert updated_job.id == job.id + assert updated_job.status == TransformationStatus.PROCESSING + assert updated_job.updated_at >= job.updated_at + + def test_can_update_status_to_completed_with_result(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test updating job status to completed with transformed document.""" + source_document = store.put() + transformed_document = store.put() + job = crud.create(source_document.id) + + updated_job = crud.update_status( + job.id, + TransformationStatus.COMPLETED, + transformed_document_id=transformed_document.id + ) + + assert updated_job.status == TransformationStatus.COMPLETED + assert updated_job.transformed_document_id == transformed_document.id + assert updated_job.error_message is None + + def test_can_update_status_to_failed_with_error(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test updating job status to failed with error message.""" + document = store.put() + job = crud.create(document.id) + error_msg = "Transformation failed due to invalid format" + + updated_job = crud.update_status( + job.id, + TransformationStatus.FAILED, + error_message=error_msg + ) + + assert updated_job.status == TransformationStatus.FAILED + assert updated_job.error_message == error_msg + assert updated_job.transformed_document_id is None + + def test_cannot_update_nonexistent_job(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test that updating a non-existent job raises an error.""" + invalid_id = next(SequentialUuidGenerator()) + + with pytest.raises(HTTPException) as exc_info: + crud.update_status(invalid_id, TransformationStatus.PROCESSING) + + assert exc_info.value.status_code == 404 + assert "Transformation job not found" in str(exc_info.value.detail) + + def test_update_preserves_existing_fields(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + """Test that updating status preserves other fields when not specified.""" + document = store.put() + job = crud.create(document.id) + + # First update with error message + crud.update_status(job.id, TransformationStatus.FAILED, error_message="Initial error") + + # Second update without error message - should preserve it + updated_job = crud.update_status(job.id, TransformationStatus.PROCESSING) + + assert updated_job.status == TransformationStatus.PROCESSING + assert updated_job.error_message == "Initial error" # Should be preserved diff --git a/backend/app/tests/utils/document.py b/backend/app/tests/utils/document.py index 6a08733b..efa7dce0 100644 --- a/backend/app/tests/utils/document.py +++ b/backend/app/tests/utils/document.py @@ -27,12 +27,12 @@ class DocumentMaker: def __init__(self, project_id: int, session: Session): self.project_id = project_id self.session = session - self.project: Project = None + self.project: Project = get_project_by_id(session=self.session, project_id=self.project_id) self.index = SequentialUuidGenerator() def __iter__(self): return self - + def __next__(self): if self.project is None: self.project = get_project_by_id( diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 219c0b03..f0dbb3a6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -2,7 +2,7 @@ name = "app" version = "0.1.0" description = "" -requires-python = ">=3.10,<4.0" +requires-python = ">=3.11,<4.0" dependencies = [ "fastapi[standard]<1.0.0,>=0.114.2", "python-multipart<1.0.0,>=0.0.7", @@ -29,6 +29,7 @@ dependencies = [ "openai_responses", "langfuse>=2.60.3", "asgi-correlation-id>=4.3.4", + "py-zerox>=0.0.7,<1.0.0" ] [tool.uv] diff --git a/backend/uv.lock b/backend/uv.lock index ce719fb4..527e09d8 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,9 +1,117 @@ version = 1 revision = 2 -requires-python = ">=3.10, <4.0" +requires-python = ">=3.11, <4.0" resolution-markers = [ - "python_full_version < '3.13'", "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, +] + +[[package]] +name = "aioshutil" +version = "1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/e4/ef86f1777a9bc0c51d50487b471644ae20941afe503591d3a4c86e456dac/aioshutil-1.5.tar.gz", hash = "sha256:2756d6cd3bb03405dc7348ac11a0b60eb949ebd63cdd15f56e922410231c1201", size = 7770, upload-time = "2024-07-20T07:02:41.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/3d/eb3c7106c4672c19280515b759b674daafd2d570c952e5491faeca157ffd/aioshutil-1.5-py3-none-any.whl", hash = "sha256:bc2a6cdcf1a8615b62f856154fd81131031d03f2834912ebb06d8a2391253652", size = 4657, upload-time = "2024-07-20T07:02:40.028Z" }, +] + +[[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]] @@ -34,10 +142,8 @@ name = "anyio" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/49/f3f17ec11c4a91fe79275c426658e509b07547f874b14c1a526d86a83fc8/anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", size = 170983, upload-time = "2024-09-21T10:33:28.479Z" } wheels = [ @@ -65,6 +171,7 @@ dependencies = [ { name = "passlib", extra = ["bcrypt"] }, { name = "pre-commit" }, { name = "psycopg", extra = ["binary"] }, + { name = "py-zerox" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyjwt" }, @@ -103,6 +210,7 @@ requires-dist = [ { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4,<2.0.0" }, { name = "pre-commit", specifier = ">=3.8.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.1.13,<4.0.0" }, + { name = "py-zerox", specifier = ">=0.0.7,<1.0.0" }, { name = "pydantic", specifier = ">2.0" }, { name = "pydantic-settings", specifier = ">=2.2.1,<3.0.0" }, { name = "pyjwt", specifier = ">=2.8.0,<3.0.0" }, @@ -136,6 +244,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl", hash = "sha256:36ce69b06c7d96b4acb89c7556a4c4f01a972463d3d49c675026cbbd08e9a0a2", size = 15262, upload-time = "2024-10-17T11:44:28.739Z" }, ] +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + [[package]] name = "backoff" version = "2.2.1" @@ -219,18 +336,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, @@ -291,21 +396,6 @@ version = "3.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload-time = "2023-11-01T04:04:59.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219, upload-time = "2023-11-01T04:02:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521, upload-time = "2023-11-01T04:02:32.452Z" }, - { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383, upload-time = "2023-11-01T04:02:34.11Z" }, - { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223, upload-time = "2023-11-01T04:02:36.213Z" }, - { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101, upload-time = "2023-11-01T04:02:38.067Z" }, - { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699, upload-time = "2023-11-01T04:02:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065, upload-time = "2023-11-01T04:02:41.357Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505, upload-time = "2023-11-01T04:02:43.108Z" }, - { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425, upload-time = "2023-11-01T04:02:45.427Z" }, - { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287, upload-time = "2023-11-01T04:02:46.705Z" }, - { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929, upload-time = "2023-11-01T04:02:48.098Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605, upload-time = "2023-11-01T04:02:49.605Z" }, - { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646, upload-time = "2023-11-01T04:02:51.35Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846, upload-time = "2023-11-01T04:02:52.679Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343, upload-time = "2023-11-01T04:02:53.915Z" }, { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647, upload-time = "2023-11-01T04:02:55.329Z" }, { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434, upload-time = "2023-11-01T04:02:57.173Z" }, { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979, upload-time = "2023-11-01T04:02:58.442Z" }, @@ -366,16 +456,6 @@ version = "7.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, - { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, - { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, - { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, - { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, - { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, - { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, @@ -416,7 +496,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, - { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, ] [[package]] @@ -452,12 +531,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631, upload-time = "2025-03-02T00:01:01.623Z" }, { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792, upload-time = "2025-03-02T00:01:04.133Z" }, { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957, upload-time = "2025-03-02T00:01:06.987Z" }, - { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886, upload-time = "2025-03-02T00:01:09.51Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387, upload-time = "2025-03-02T00:01:11.348Z" }, - { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922, upload-time = "2025-03-02T00:01:13.934Z" }, - { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715, upload-time = "2025-03-02T00:01:16.895Z" }, - { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876, upload-time = "2025-03-02T00:01:18.751Z" }, - { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719, upload-time = "2025-03-02T00:01:21.269Z" }, { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513, upload-time = "2025-03-02T00:01:22.911Z" }, { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432, upload-time = "2025-03-02T00:01:24.701Z" }, { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421, upload-time = "2025-03-02T00:01:26.335Z" }, @@ -542,15 +615,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/7e/b648d640d88d31de49e566832aca9cce025c52d6349b0a0fc65e9df1f4c5/emails-0.6-py2.py3-none-any.whl", hash = "sha256:72c1e3198075709cc35f67e1b49e2da1a2bc087e9b444073db61a379adfb7f3c", size = 56250, upload-time = "2020-06-19T11:20:40.466Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, -] - [[package]] name = "fastapi" version = "0.115.0" @@ -602,21 +666,98 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, ] +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, + { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, + { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, + { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432, upload-time = "2025-07-15T16:05:21.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload-time = "2025-07-15T16:05:19.529Z" }, +] + [[package]] name = "greenlet" version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235, upload-time = "2024-09-20T17:07:18.761Z" }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168, upload-time = "2024-09-20T17:36:43.774Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826, upload-time = "2024-09-20T17:39:16.921Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443, upload-time = "2024-09-20T17:44:21.896Z" }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295, upload-time = "2024-09-20T17:08:37.951Z" }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544, upload-time = "2024-09-20T17:08:27.894Z" }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456, upload-time = "2024-09-20T17:44:11.755Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111, upload-time = "2024-09-20T17:09:22.104Z" }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392, upload-time = "2024-09-20T17:28:51.988Z" }, { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, @@ -662,6 +803,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, ] +[[package]] +name = "hf-xet" +version = "1.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/0a/a0f56735940fde6dd627602fec9ab3bad23f66a272397560abd65aba416e/hf_xet-1.1.7.tar.gz", hash = "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80", size = 477719, upload-time = "2025-08-06T00:30:55.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/7c/8d7803995caf14e7d19a392a486a040f923e2cfeff824e9b800b92072f76/hf_xet-1.1.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde", size = 2761743, upload-time = "2025-08-06T00:30:50.634Z" }, + { url = "https://files.pythonhosted.org/packages/51/a3/fa5897099454aa287022a34a30e68dbff0e617760f774f8bd1db17f06bd4/hf_xet-1.1.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e", size = 2624331, upload-time = "2025-08-06T00:30:49.212Z" }, + { url = "https://files.pythonhosted.org/packages/86/50/2446a132267e60b8a48b2e5835d6e24fd988000d0f5b9b15ebd6d64ef769/hf_xet-1.1.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f", size = 3183844, upload-time = "2025-08-06T00:30:47.582Z" }, + { url = "https://files.pythonhosted.org/packages/20/8f/ccc670616bb9beee867c6bb7139f7eab2b1370fe426503c25f5cbb27b148/hf_xet-1.1.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513", size = 3074209, upload-time = "2025-08-06T00:30:45.509Z" }, + { url = "https://files.pythonhosted.org/packages/21/0a/4c30e1eb77205565b854f5e4a82cf1f056214e4dc87f2918ebf83d47ae14/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8", size = 3239602, upload-time = "2025-08-06T00:30:52.41Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1e/fc7e9baf14152662ef0b35fa52a6e889f770a7ed14ac239de3c829ecb47e/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604", size = 3348184, upload-time = "2025-08-06T00:30:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/e354eae84ceff117ec3560141224724794828927fcc013c5b449bf0b8745/hf_xet-1.1.7-cp37-abi3-win_amd64.whl", hash = "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34", size = 2820008, upload-time = "2025-08-06T00:30:57.056Z" }, +] + [[package]] name = "httpcore" version = "1.0.5" @@ -681,13 +837,6 @@ version = "0.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/67/1d/d77686502fced061b3ead1c35a2d70f6b281b5f723c4eff7a2277c04e4a2/httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a", size = 191228, upload-time = "2023-10-16T17:42:36.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/6a/80bce0216b63babf51cdc34814c3f0f10489e13ab89fb6bc91202736a8a2/httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f", size = 149778, upload-time = "2023-10-16T17:41:35.97Z" }, - { url = "https://files.pythonhosted.org/packages/bd/7d/4cd75356dfe0ed0b40ca6873646bf9ff7b5138236c72338dc569dc57d509/httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563", size = 77604, upload-time = "2023-10-16T17:41:38.361Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/6348ce41fb5c1484f35184c172efb8854a288e6090bb54e2210598268369/httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58", size = 346717, upload-time = "2023-10-16T17:41:40.447Z" }, - { url = "https://files.pythonhosted.org/packages/65/e7/dd5ba95c84047118a363f0755ad78e639e0529be92424bb020496578aa3b/httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185", size = 341442, upload-time = "2023-10-16T17:41:42.492Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/b37d596bc32be291477a8912bf9d1508d7e8553aa11a30cd871fd89cbae4/httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142", size = 354531, upload-time = "2023-10-16T17:41:44.488Z" }, - { url = "https://files.pythonhosted.org/packages/99/c9/53ed7176583ec4b4364d941a08624288f2ae55b4ff58b392cdb68db1e1ed/httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658", size = 347754, upload-time = "2023-10-16T17:41:46.567Z" }, - { url = "https://files.pythonhosted.org/packages/1e/fc/8a26c2adcd3f141e4729897633f03832b71ebea6f4c31cce67a92ded1961/httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b", size = 58165, upload-time = "2023-10-16T17:41:48.859Z" }, { url = "https://files.pythonhosted.org/packages/f5/d1/53283b96ed823d5e4d89ee9aa0f29df5a1bdf67f148e061549a595d534e4/httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1", size = 145855, upload-time = "2023-10-16T17:41:50.407Z" }, { url = "https://files.pythonhosted.org/packages/80/dd/cebc9d4b1d4b70e9f3d40d1db0829a28d57ca139d0b04197713816a11996/httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0", size = 75604, upload-time = "2023-10-16T17:41:52.204Z" }, { url = "https://files.pythonhosted.org/packages/76/7a/45c5a9a2e9d21f7381866eb7b6ead5a84d8fe7e54e35208eeb18320a29b4/httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc", size = 324784, upload-time = "2023-10-16T17:41:53.617Z" }, @@ -720,6 +869,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, ] +[[package]] +name = "huggingface-hub" +version = "0.34.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/c9/bdbe19339f76d12985bc03572f330a01a93c04dffecaaea3061bdd7fb892/huggingface_hub-0.34.4.tar.gz", hash = "sha256:a4228daa6fb001be3f4f4bdaf9a0db00e1739235702848df00885c9b5742c85c", size = 459768, upload-time = "2025-08-08T09:14:52.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/7b/bb06b061991107cd8783f300adff3e7b7f284e330fd82f507f2a1417b11d/huggingface_hub-0.34.4-py3-none-any.whl", hash = "sha256:9b365d781739c93ff90c359844221beef048403f1bc1f1c123c191257c3c890a", size = 561452, upload-time = "2025-08-08T09:14:50.159Z" }, +] + [[package]] name = "identify" version = "2.6.1" @@ -738,6 +906,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -765,18 +945,6 @@ version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604, upload-time = "2025-03-10T21:37:03.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/82/39f7c9e67b3b0121f02a0b90d433626caa95a565c3d2449fea6bcfa3f5f5/jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad", size = 314540, upload-time = "2025-03-10T21:35:02.218Z" }, - { url = "https://files.pythonhosted.org/packages/01/07/7bf6022c5a152fca767cf5c086bb41f7c28f70cf33ad259d023b53c0b858/jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea", size = 321065, upload-time = "2025-03-10T21:35:04.274Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b2/de3f3446ecba7c48f317568e111cc112613da36c7b29a6de45a1df365556/jiter-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1339f839b91ae30b37c409bf16ccd3dc453e8b8c3ed4bd1d6a567193651a4a51", size = 341664, upload-time = "2025-03-10T21:35:06.032Z" }, - { url = "https://files.pythonhosted.org/packages/13/cf/6485a4012af5d407689c91296105fcdb080a3538e0658d2abf679619c72f/jiter-0.9.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ffba79584b3b670fefae66ceb3a28822365d25b7bf811e030609a3d5b876f538", size = 364635, upload-time = "2025-03-10T21:35:07.749Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f7/4a491c568f005553240b486f8e05c82547340572d5018ef79414b4449327/jiter-0.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cfc7d0a8e899089d11f065e289cb5b2daf3d82fbe028f49b20d7b809193958d", size = 406288, upload-time = "2025-03-10T21:35:09.238Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ca/f4263ecbce7f5e6bded8f52a9f1a66540b270c300b5c9f5353d163f9ac61/jiter-0.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e00a1a2bbfaaf237e13c3d1592356eab3e9015d7efd59359ac8b51eb56390a12", size = 397499, upload-time = "2025-03-10T21:35:12.463Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a2/522039e522a10bac2f2194f50e183a49a360d5f63ebf46f6d890ef8aa3f9/jiter-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1d9870561eb26b11448854dce0ff27a9a27cb616b632468cafc938de25e9e51", size = 352926, upload-time = "2025-03-10T21:35:13.85Z" }, - { url = "https://files.pythonhosted.org/packages/b1/67/306a5c5abc82f2e32bd47333a1c9799499c1c3a415f8dde19dbf876f00cb/jiter-0.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9872aeff3f21e437651df378cb75aeb7043e5297261222b6441a620218b58708", size = 384506, upload-time = "2025-03-10T21:35:15.735Z" }, - { url = "https://files.pythonhosted.org/packages/0f/89/c12fe7b65a4fb74f6c0d7b5119576f1f16c79fc2953641f31b288fad8a04/jiter-0.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fd19112d1049bdd47f17bfbb44a2c0001061312dcf0e72765bfa8abd4aa30e5", size = 520621, upload-time = "2025-03-10T21:35:17.55Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2b/d57900c5c06e6273fbaa76a19efa74dbc6e70c7427ab421bf0095dfe5d4a/jiter-0.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef5da104664e526836070e4a23b5f68dec1cc673b60bf1edb1bfbe8a55d0678", size = 512613, upload-time = "2025-03-10T21:35:19.178Z" }, - { url = "https://files.pythonhosted.org/packages/89/05/d8b90bfb21e58097d5a4e0224f2940568366f68488a079ae77d4b2653500/jiter-0.9.0-cp310-cp310-win32.whl", hash = "sha256:cb12e6d65ebbefe5518de819f3eda53b73187b7089040b2d17f5b39001ff31c4", size = 206613, upload-time = "2025-03-10T21:35:21.039Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1d/5767f23f88e4f885090d74bbd2755518050a63040c0f59aa059947035711/jiter-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:c43ca669493626d8672be3b645dbb406ef25af3f4b6384cfd306da7eb2e70322", size = 208371, upload-time = "2025-03-10T21:35:22.536Z" }, { url = "https://files.pythonhosted.org/packages/23/44/e241a043f114299254e44d7e777ead311da400517f179665e59611ab0ee4/jiter-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6c4d99c71508912a7e556d631768dcdef43648a93660670986916b297f1c54af", size = 314654, upload-time = "2025-03-10T21:35:23.939Z" }, { url = "https://files.pythonhosted.org/packages/fb/1b/a7e5e42db9fa262baaa9489d8d14ca93f8663e7f164ed5e9acc9f467fc00/jiter-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f60fb8ce7df529812bf6c625635a19d27f30806885139e367af93f6e734ef58", size = 320909, upload-time = "2025-03-10T21:35:26.127Z" }, { url = "https://files.pythonhosted.org/packages/60/bf/8ebdfce77bc04b81abf2ea316e9c03b4a866a7d739cf355eae4d6fd9f6fe/jiter-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51c4e1a4f8ea84d98b7b98912aa4290ac3d1eabfde8e3c34541fae30e9d1f08b", size = 341733, upload-time = "2025-03-10T21:35:27.94Z" }, @@ -827,6 +995,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, ] +[[package]] +name = "jsonschema" +version = "4.25.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/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + [[package]] name = "langfuse" version = "2.60.3" @@ -846,29 +1041,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/6b/4d3bdea30ceb3e4cf3ac1a2f104ffc20b6caa636549874262b2fa8cedaec/langfuse-2.60.3-py3-none-any.whl", hash = "sha256:2b866c44f24d5f06b617d7f14f75a2e42577538b530e4e26dc6ad770d6d1399e", size = 275008, upload-time = "2025-04-15T17:01:13.799Z" }, ] +[[package]] +name = "litellm" +version = "1.65.0.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/94/d4d1c3f7f7475ca6b24284e840d516c7b882b8055f5820d6a6e455c8bfb3/litellm-1.65.0.post1.tar.gz", hash = "sha256:87bd42b834fbe61b4cfe80d5dce7bb2e63a8a067b66bd5a5fb7647536d26ea1a", size = 6662760, upload-time = "2025-04-01T19:00:41.651Z" } + [[package]] name = "lxml" version = "5.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318, upload-time = "2024-08-10T18:17:29.668Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ce/2789e39eddf2b13fac29878bfa465f0910eb6b0096e29090e5176bc8cf43/lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", size = 8124570, upload-time = "2024-08-10T18:09:04.096Z" }, - { url = "https://files.pythonhosted.org/packages/24/a8/f4010166a25d41715527129af2675981a50d3bbf7df09c5d9ab8ca24fbf9/lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", size = 4413042, upload-time = "2024-08-10T18:09:08.841Z" }, - { url = "https://files.pythonhosted.org/packages/41/a4/7e45756cecdd7577ddf67a68b69c1db0f5ddbf0c9f65021ee769165ffc5a/lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", size = 5139213, upload-time = "2024-08-10T18:09:12.622Z" }, - { url = "https://files.pythonhosted.org/packages/02/e2/ecf845b12323c92748077e1818b64e8b4dba509a4cb12920b3762ebe7552/lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8", size = 4838814, upload-time = "2024-08-10T18:09:16.222Z" }, - { url = "https://files.pythonhosted.org/packages/12/91/619f9fb72cf75e9ceb8700706f7276f23995f6ad757e6d400fbe35ca4990/lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", size = 5425084, upload-time = "2024-08-10T18:09:19.795Z" }, - { url = "https://files.pythonhosted.org/packages/25/3b/162a85a8f0fd2a3032ec3f936636911c6e9523a8e263fffcfd581ce98b54/lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", size = 4875993, upload-time = "2024-08-10T18:09:23.776Z" }, - { url = "https://files.pythonhosted.org/packages/43/af/dd3f58cc7d946da6ae42909629a2b1d5dd2d1b583334d4af9396697d6863/lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", size = 5012462, upload-time = "2024-08-10T18:09:27.642Z" }, - { url = "https://files.pythonhosted.org/packages/69/c1/5ea46b2d4c98f5bf5c83fffab8a0ad293c9bc74df9ecfbafef10f77f7201/lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", size = 4815288, upload-time = "2024-08-10T18:09:31.633Z" }, - { url = "https://files.pythonhosted.org/packages/1d/51/a0acca077ad35da458f4d3f729ef98effd2b90f003440d35fc36323f8ae6/lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", size = 5472435, upload-time = "2024-08-10T18:09:35.758Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6b/0989c9368986961a6b0f55b46c80404c4b758417acdb6d87bfc3bd5f4967/lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", size = 4976354, upload-time = "2024-08-10T18:09:39.51Z" }, - { url = "https://files.pythonhosted.org/packages/05/9e/87492d03ff604fbf656ed2bf3e2e8d28f5d58ea1f00ff27ac27b06509079/lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", size = 5029973, upload-time = "2024-08-10T18:09:42.978Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cc/9ae1baf5472af88e19e2c454b3710c1be9ecafb20eb474eeabcd88a055d2/lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", size = 4888837, upload-time = "2024-08-10T18:09:46.185Z" }, - { url = "https://files.pythonhosted.org/packages/d2/10/5594ffaec8c120d75b17e3ad23439b740a51549a9b5fd7484b2179adfe8f/lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", size = 5530555, upload-time = "2024-08-10T18:09:50.366Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9b/de17f05377c8833343b629905571fb06cff2028f15a6f58ae2267662e341/lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", size = 5405314, upload-time = "2024-08-10T18:09:54.58Z" }, - { url = "https://files.pythonhosted.org/packages/8a/b4/227be0f1f3cca8255925985164c3838b8b36e441ff0cc10c1d3c6bdba031/lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", size = 5079303, upload-time = "2024-08-10T18:09:58.032Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ee/19abcebb7fc40319bb71cd6adefa1ad94d09b5660228715854d6cc420713/lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", size = 3475126, upload-time = "2024-08-10T18:10:01.43Z" }, - { url = "https://files.pythonhosted.org/packages/a1/35/183d32551447e280032b2331738cd850da435a42f850b71ebeaab42c1313/lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", size = 3805065, upload-time = "2024-08-10T18:10:05.189Z" }, { url = "https://files.pythonhosted.org/packages/5c/a8/449faa2a3cbe6a99f8d38dcd51a3ee8844c17862841a6f769ea7c2a9cd0f/lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", size = 8141056, upload-time = "2024-08-10T18:10:09.455Z" }, { url = "https://files.pythonhosted.org/packages/ac/8a/ae6325e994e2052de92f894363b038351c50ee38749d30cc6b6d96aaf90f/lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", size = 4425238, upload-time = "2024-08-10T18:10:13.348Z" }, { url = "https://files.pythonhosted.org/packages/f8/fb/128dddb7f9086236bce0eeae2bfb316d138b49b159f50bc681d56c1bdd19/lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", size = 5095197, upload-time = "2024-08-10T18:10:16.825Z" }, @@ -920,12 +1117,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/17/71e9984cf0570cd202ac0a1c9ed5c1b8889b0fc8dc736f5ef0ffb181c284/lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", size = 5011053, upload-time = "2024-08-10T18:12:59.714Z" }, { url = "https://files.pythonhosted.org/packages/69/68/9f7e6d3312a91e30829368c2b3217e750adef12a6f8eb10498249f4e8d72/lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", size = 3485634, upload-time = "2024-08-10T18:13:02.78Z" }, { url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417, upload-time = "2024-08-10T18:13:05.791Z" }, - { url = "https://files.pythonhosted.org/packages/99/f7/b73a431c8500565aa500e99e60b448d305eaf7c0b4c893c7c5a8a69cc595/lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", size = 3925431, upload-time = "2024-08-10T18:15:59.002Z" }, - { url = "https://files.pythonhosted.org/packages/db/48/4a206623c0d093d0e3b15f415ffb4345b0bdf661a3d0b15a112948c033c7/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", size = 4216683, upload-time = "2024-08-10T18:16:03.004Z" }, - { url = "https://files.pythonhosted.org/packages/54/47/577820c45dd954523ae8453b632d91e76da94ca6d9ee40d8c98dd86f916b/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", size = 4326732, upload-time = "2024-08-10T18:16:06.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/de/96cb6d3269bc994b4f5ede8ca7bf0840f5de0a278bc6e50cb317ff71cafa/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", size = 4218377, upload-time = "2024-08-10T18:16:10.836Z" }, - { url = "https://files.pythonhosted.org/packages/a5/43/19b1ef6cbffa4244a217f95cc5f41a6cb4720fed33510a49670b03c5f1a0/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", size = 4351237, upload-time = "2024-08-10T18:16:14.652Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b2/6a22fb5c0885da3b00e116aee81f0b829ec9ac8f736cd414b4a09413fc7d/lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", size = 3487557, upload-time = "2024-08-10T18:16:18.255Z" }, ] [[package]] @@ -958,16 +1149,6 @@ version = "2.1.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, - { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, - { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, - { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, - { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, - { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, @@ -1034,22 +1215,97 @@ s3 = [ { name = "pyyaml" }, ] +[[package]] +name = "multidict" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445, upload-time = "2025-06-30T15:51:24.01Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610, upload-time = "2025-06-30T15:51:25.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267, upload-time = "2025-06-30T15:51:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004, upload-time = "2025-06-30T15:51:27.491Z" }, + { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196, upload-time = "2025-06-30T15:51:28.762Z" }, + { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337, upload-time = "2025-06-30T15:51:30.025Z" }, + { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079, upload-time = "2025-06-30T15:51:31.716Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461, upload-time = "2025-06-30T15:51:33.029Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611, upload-time = "2025-06-30T15:51:34.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102, upload-time = "2025-06-30T15:51:36.525Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693, upload-time = "2025-06-30T15:51:38.278Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582, upload-time = "2025-06-30T15:51:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355, upload-time = "2025-06-30T15:51:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774, upload-time = "2025-06-30T15:51:42.291Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275, upload-time = "2025-06-30T15:51:43.642Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290, upload-time = "2025-06-30T15:51:45.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942, upload-time = "2025-06-30T15:51:46.377Z" }, + { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880, upload-time = "2025-06-30T15:51:47.561Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, + { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, + { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" }, + { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" }, + { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" }, + { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" }, + { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" }, + { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" }, + { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" }, + { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, + { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, +] + [[package]] name = "mypy" version = "1.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806, upload-time = "2024-08-24T22:50:11.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401, upload-time = "2024-08-24T22:49:18.929Z" }, - { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697, upload-time = "2024-08-24T22:49:32.504Z" }, - { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508, upload-time = "2024-08-24T22:49:12.327Z" }, - { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712, upload-time = "2024-08-24T22:49:49.399Z" }, - { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319, upload-time = "2024-08-24T22:49:26.88Z" }, { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630, upload-time = "2024-08-24T22:49:51.895Z" }, { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973, upload-time = "2024-08-24T22:49:21.428Z" }, { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659, upload-time = "2024-08-24T22:49:35.02Z" }, @@ -1137,6 +1393,102 @@ bcrypt = [ { name = "bcrypt" }, ] +[[package]] +name = "pdf2image" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/d8/b280f01045555dc257b8153c00dee3bc75830f91a744cd5f84ef3a0a64b1/pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57", size = 12811, upload-time = "2024-01-07T20:33:01.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/33/61766ae033518957f877ab246f87ca30a85b778ebaad65b7f74fa7e52988/pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2", size = 11618, upload-time = "2024-01-07T20:32:59.957Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -1187,6 +1539,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/07/4e8d94f94c7d41ca5ddf8a9695ad87b888104e2fd41a35546c1dc9ca74ac/premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a", size = 19544, upload-time = "2021-08-02T20:32:52.771Z" }, ] +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + [[package]] name = "psycopg" version = "3.2.2" @@ -1210,17 +1635,6 @@ name = "psycopg-binary" version = "3.2.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/42/f5a181d07c0ae5c8091449fda45d562d3b0861c127b94d7009eaea45c61f/psycopg_binary-3.2.2-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8eacbf58d4f8d7bc82e0a60476afa2622b5a58f639a3cc2710e3e37b72aff3cb", size = 3381668, upload-time = "2024-09-15T20:40:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fb/66d2e3e5d550ba3b9d33e30bf6d5beb871a85eb95553c851fce7f09f8a1e/psycopg_binary-3.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d07e62476ee8c54853b2b8cfdf3858a574218103b4cd213211f64326c7812437", size = 3502272, upload-time = "2024-09-15T20:40:44.934Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8d/758da39eca57f046ee712ad4c310840bcc08d889042d1b297cd28c78e909/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c22e615ee0ecfc6687bb8a39a4ed9d6bac030b5e72ac15e7324fd6e48979af71", size = 4467251, upload-time = "2024-09-15T20:41:00.229Z" }, - { url = "https://files.pythonhosted.org/packages/91/bb/1abb1ccc318eb878acf9637479334de7406529516126e4af48b16dd85426/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec29c7ec136263628e3f09a53e51d0a4b1ad765a6e45135707bfa848b39113f9", size = 4268614, upload-time = "2024-09-15T20:41:16.305Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1a/14b4ae68f1c7cfba543883987d2f134eca31b0983bb684a52e0f51f3ac21/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:035753f80cbbf6aceca6386f53e139df70c7aca057b0592711047b5a8cfef8bb", size = 4512352, upload-time = "2024-09-15T20:42:04.018Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/53df01c7c7cffb351cafa88c58692fab0ab962edd89f22974cbfc38b6677/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ee99336151ff7c30682f2ef9cb1174d235bc1471322faabba97f9db1398167", size = 4212477, upload-time = "2024-09-15T20:51:45.68Z" }, - { url = "https://files.pythonhosted.org/packages/b7/31/c918927692fc5a9c4db0a7c454e1595e9d40378d5c526d26505f310e4068/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a60674dff4a4194e88312b463fb84ac80924c2b9e25d0e0460f3176bf1af4a6b", size = 3137907, upload-time = "2024-09-15T20:51:58.211Z" }, - { url = "https://files.pythonhosted.org/packages/cb/65/538aa057b3e8245a31ea8baac93df9947ee1b2ebf4c02014a556cddd875e/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3c701507a49340de422d77a6ce95918a0019990bbf27daec35aa40050c6eadb6", size = 3113363, upload-time = "2024-09-15T20:52:35.883Z" }, - { url = "https://files.pythonhosted.org/packages/dc/81/eaee4f05bcba19984615e90319c429d125d07e5f0fe8c8ec3025901de4df/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b3c5a04eaf8866e399315cff2e810260cce10b797437a9f49fd71b5f4b94d0a", size = 3220512, upload-time = "2024-09-15T20:52:48.037Z" }, - { url = "https://files.pythonhosted.org/packages/48/cc/1d0f82a47216f925e36be6f6d7be61984a5168ff8c0496c57f468cc0e219/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ad9c09de4c262f516ae6891d042a4325649b18efa39dd82bbe0f7bc95c37bfb", size = 3255023, upload-time = "2024-09-15T20:53:00.3Z" }, - { url = "https://files.pythonhosted.org/packages/d0/29/c45760ba6218eae37474aa5f46c1f55b290a6d4b86c0c59e60fa5613257a/psycopg_binary-3.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:bf1d3582185cb43ecc27403bee2f5405b7a45ccaab46c8508d9a9327341574fc", size = 2921688, upload-time = "2024-09-15T20:53:16.852Z" }, { url = "https://files.pythonhosted.org/packages/1f/1a/76299ad86a01f57a67961c4a45ce06c6eb8e76b8bc7bfb92548c62a6fa72/psycopg_binary-3.2.2-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:554d208757129d34fa47b7c890f9ef922f754e99c6b089cb3a209aa0fe282682", size = 3390336, upload-time = "2024-09-15T20:53:41.564Z" }, { url = "https://files.pythonhosted.org/packages/c2/1d/04fbcadd568eb0ee04b0d99286fe4ffd6c76c9cdd130e58d477617b77941/psycopg_binary-3.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:71dc3cc10d1fd7d26a3079d0a5b4a8e8ad0d7b89a702ceb7605a52e4395be122", size = 3507406, upload-time = "2024-09-15T20:53:51.312Z" }, { url = "https://files.pythonhosted.org/packages/60/00/094a437f68d83fef4dd139630dfb0e060fcf2a7ac68fffdb63b2f3eaa43a/psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86f578d63f2e1fdf87c9adaed4ff23d7919bda8791cf1380fa4cf3a857ccb8b", size = 4463745, upload-time = "2024-09-15T20:54:22.632Z" }, @@ -1265,6 +1679,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/84/0e410c20bbe9a504fc56e97908f13261c2b313d16cbb3b738556166f044a/py_partiql_parser-0.6.1-py2.py3-none-any.whl", hash = "sha256:ff6a48067bff23c37e9044021bf1d949c83e195490c17e020715e927fe5b2456", size = 23520, upload-time = "2024-12-25T22:06:39.106Z" }, ] +[[package]] +name = "py-zerox" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "aioshutil" }, + { name = "litellm" }, + { name = "pdf2image" }, + { name = "pypdf2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/8d/515f829b9f0aacccad83bb4b6c8bfc4a0ba3ea89604afbb3a80e283e79c2/py_zerox-0.0.7.tar.gz", hash = "sha256:8f84f9b97abcd88ba96d820e4f40b94c40c3c51104a6d13c3f404ddaeaaafdad", size = 18870, upload-time = "2024-10-21T16:03:35.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/60/22c9716033ced1ee1d800457126c4c79652a4ed635b0554c1d93742cc0a1/py_zerox-0.0.7-py3-none-any.whl", hash = "sha256:7b7d92cb6fafec91a94b63ba3c039b643fb3ee83545b15fa330ec07dd52f2058", size = 23347, upload-time = "2024-10-21T16:03:33.406Z" }, +] + [[package]] name = "pycparser" version = "2.22" @@ -1297,18 +1728,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156, upload-time = "2024-09-16T16:06:44.786Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/8b/d3ae387f66277bd8104096d6ec0a145f4baa2966ebb2cad746c0920c9526/pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", size = 1867835, upload-time = "2024-09-16T16:03:57.223Z" }, - { url = "https://files.pythonhosted.org/packages/46/76/f68272e4c3a7df8777798282c5e47d508274917f29992d84e1898f8908c7/pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", size = 1776689, upload-time = "2024-09-16T16:03:59.266Z" }, - { url = "https://files.pythonhosted.org/packages/cc/69/5f945b4416f42ea3f3bc9d2aaec66c76084a6ff4ff27555bf9415ab43189/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", size = 1800748, upload-time = "2024-09-16T16:04:01.011Z" }, - { url = "https://files.pythonhosted.org/packages/50/ab/891a7b0054bcc297fb02d44d05c50e68154e31788f2d9d41d0b72c89fdf7/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", size = 1806469, upload-time = "2024-09-16T16:04:02.323Z" }, - { url = "https://files.pythonhosted.org/packages/31/7c/6e3fa122075d78f277a8431c4c608f061881b76c2b7faca01d317ee39b5d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", size = 2002246, upload-time = "2024-09-16T16:04:03.688Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6f/22d5692b7ab63fc4acbc74de6ff61d185804a83160adba5e6cc6068e1128/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", size = 2659404, upload-time = "2024-09-16T16:04:05.299Z" }, - { url = "https://files.pythonhosted.org/packages/11/ac/1e647dc1121c028b691028fa61a4e7477e6aeb5132628fde41dd34c1671f/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", size = 2053940, upload-time = "2024-09-16T16:04:06.604Z" }, - { url = "https://files.pythonhosted.org/packages/91/75/984740c17f12c3ce18b5a2fcc4bdceb785cce7df1511a4ce89bca17c7e2d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", size = 1921437, upload-time = "2024-09-16T16:04:08.071Z" }, - { url = "https://files.pythonhosted.org/packages/a0/74/13c5f606b64d93f0721e7768cd3e8b2102164866c207b8cd6f90bb15d24f/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", size = 1966129, upload-time = "2024-09-16T16:04:10.363Z" }, - { url = "https://files.pythonhosted.org/packages/18/03/9c4aa5919457c7b57a016c1ab513b1a926ed9b2bb7915bf8e506bf65c34b/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", size = 2110908, upload-time = "2024-09-16T16:04:12.412Z" }, - { url = "https://files.pythonhosted.org/packages/92/2c/053d33f029c5dc65e5cf44ff03ceeefb7cce908f8f3cca9265e7f9b540c8/pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", size = 1735278, upload-time = "2024-09-16T16:04:13.732Z" }, - { url = "https://files.pythonhosted.org/packages/de/81/7dfe464eca78d76d31dd661b04b5f2036ec72ea8848dd87ab7375e185c23/pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", size = 1917453, upload-time = "2024-09-16T16:04:15.996Z" }, { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160, upload-time = "2024-09-16T16:04:18.628Z" }, { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777, upload-time = "2024-09-16T16:04:20.038Z" }, { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244, upload-time = "2024-09-16T16:04:21.799Z" }, @@ -1345,14 +1764,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814, upload-time = "2024-09-16T16:05:15.684Z" }, { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360, upload-time = "2024-09-16T16:05:17.258Z" }, { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411, upload-time = "2024-09-16T16:05:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/13/a9/5d582eb3204464284611f636b55c0a7410d748ff338756323cb1ce721b96/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", size = 1857135, upload-time = "2024-09-16T16:06:10.45Z" }, - { url = "https://files.pythonhosted.org/packages/2c/57/faf36290933fe16717f97829eabfb1868182ac495f99cf0eda9f59687c9d/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", size = 1740583, upload-time = "2024-09-16T16:06:12.298Z" }, - { url = "https://files.pythonhosted.org/packages/91/7c/d99e3513dc191c4fec363aef1bf4c8af9125d8fa53af7cb97e8babef4e40/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", size = 1793637, upload-time = "2024-09-16T16:06:14.092Z" }, - { url = "https://files.pythonhosted.org/packages/29/18/812222b6d18c2d13eebbb0f7cdc170a408d9ced65794fdb86147c77e1982/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", size = 1941963, upload-time = "2024-09-16T16:06:16.757Z" }, - { url = "https://files.pythonhosted.org/packages/0f/36/c1f3642ac3f05e6bb4aec3ffc399fa3f84895d259cf5f0ce3054b7735c29/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", size = 1915332, upload-time = "2024-09-16T16:06:18.677Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ca/9c0854829311fb446020ebb540ee22509731abad886d2859c855dd29b904/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", size = 1957926, upload-time = "2024-09-16T16:06:20.591Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1c/7836b67c42d0cd4441fcd9fafbf6a027ad4b79b6559f80cf11f89fd83648/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", size = 2100342, upload-time = "2024-09-16T16:06:22.888Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f9/b6bcaf874f410564a78908739c80861a171788ef4d4f76f5009656672dfe/pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", size = 1920344, upload-time = "2024-09-16T16:06:24.849Z" }, ] [[package]] @@ -1386,17 +1797,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344, upload-time = "2024-08-01T15:01:06.481Z" }, ] +[[package]] +name = "pypdf2" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419, upload-time = "2022-12-31T10:36:13.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572, upload-time = "2022-12-31T10:36:10.327Z" }, +] + [[package]] name = "pytest" version = "7.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } wheels = [ @@ -1439,15 +1857,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, @@ -1477,6 +1886,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "referencing" +version = "0.36.2" +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/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "regex" +version = "2025.7.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714, upload-time = "2025-07-31T00:21:16.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/85/f497b91577169472f7c1dc262a5ecc65e39e146fc3a52c571e5daaae4b7d/regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8", size = 484594, upload-time = "2025-07-31T00:19:13.927Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c5/ad2a5c11ce9e6257fcbfd6cd965d07502f6054aaa19d50a3d7fd991ec5d1/regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a", size = 289294, upload-time = "2025-07-31T00:19:15.395Z" }, + { url = "https://files.pythonhosted.org/packages/8e/01/83ffd9641fcf5e018f9b51aa922c3e538ac9439424fda3df540b643ecf4f/regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68", size = 285933, upload-time = "2025-07-31T00:19:16.704Z" }, + { url = "https://files.pythonhosted.org/packages/77/20/5edab2e5766f0259bc1da7381b07ce6eb4401b17b2254d02f492cd8a81a8/regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78", size = 792335, upload-time = "2025-07-31T00:19:18.561Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/744d3ed8777dce8487b2606b94925e207e7c5931d5870f47f5b643a4580a/regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719", size = 858605, upload-time = "2025-07-31T00:19:20.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/93754176289718d7578c31d151047e7b8acc7a8c20e7706716f23c49e45e/regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33", size = 905780, upload-time = "2025-07-31T00:19:21.876Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/c689f274a92deffa03999a430505ff2aeace408fd681a90eafa92fdd6930/regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083", size = 798868, upload-time = "2025-07-31T00:19:23.222Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/39673688805d139b33b4a24851a71b9978d61915c4d72b5ffda324d0668a/regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3", size = 781784, upload-time = "2025-07-31T00:19:24.59Z" }, + { url = "https://files.pythonhosted.org/packages/18/bd/4c1cab12cfabe14beaa076523056b8ab0c882a8feaf0a6f48b0a75dab9ed/regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d", size = 852837, upload-time = "2025-07-31T00:19:25.911Z" }, + { url = "https://files.pythonhosted.org/packages/cb/21/663d983cbb3bba537fc213a579abbd0f263fb28271c514123f3c547ab917/regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd", size = 844240, upload-time = "2025-07-31T00:19:27.688Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2d/9beeeb913bc5d32faa913cf8c47e968da936af61ec20af5d269d0f84a100/regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a", size = 787139, upload-time = "2025-07-31T00:19:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f5/9b9384415fdc533551be2ba805dd8c4621873e5df69c958f403bfd3b2b6e/regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1", size = 264019, upload-time = "2025-07-31T00:19:31.129Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/e069ed94debcf4cc9626d652a48040b079ce34c7e4fb174f16874958d485/regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a", size = 276047, upload-time = "2025-07-31T00:19:32.497Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/3bafbe9d1fd1db77355e7fbbbf0d0cfb34501a8b8e334deca14f94c7b315/regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0", size = 268362, upload-time = "2025-07-31T00:19:34.094Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f0/31d62596c75a33f979317658e8d261574785c6cd8672c06741ce2e2e2070/regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50", size = 485492, upload-time = "2025-07-31T00:19:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/d8/16/b818d223f1c9758c3434be89aa1a01aae798e0e0df36c1f143d1963dd1ee/regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f", size = 290000, upload-time = "2025-07-31T00:19:37.175Z" }, + { url = "https://files.pythonhosted.org/packages/cd/70/69506d53397b4bd6954061bae75677ad34deb7f6ca3ba199660d6f728ff5/regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130", size = 286072, upload-time = "2025-07-31T00:19:38.612Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/536a216d5f66084fb577bb0543b5cb7de3272eb70a157f0c3a542f1c2551/regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46", size = 797341, upload-time = "2025-07-31T00:19:40.119Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/733f8168449e56e8f404bb807ea7189f59507cbea1b67a7bbcd92f8bf844/regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4", size = 862556, upload-time = "2025-07-31T00:19:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/19/dd/59c464d58c06c4f7d87de4ab1f590e430821345a40c5d345d449a636d15f/regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0", size = 910762, upload-time = "2025-07-31T00:19:43Z" }, + { url = "https://files.pythonhosted.org/packages/37/a8/b05ccf33ceca0815a1e253693b2c86544932ebcc0049c16b0fbdf18b688b/regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b", size = 801892, upload-time = "2025-07-31T00:19:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/b993cb2e634cc22810afd1652dba0cae156c40d4864285ff486c73cd1996/regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01", size = 786551, upload-time = "2025-07-31T00:19:46.127Z" }, + { url = "https://files.pythonhosted.org/packages/2d/79/7849d67910a0de4e26834b5bb816e028e35473f3d7ae563552ea04f58ca2/regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77", size = 856457, upload-time = "2025-07-31T00:19:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/91/c6/de516bc082524b27e45cb4f54e28bd800c01efb26d15646a65b87b13a91e/regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da", size = 848902, upload-time = "2025-07-31T00:19:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/519ff8ba15f732db099b126f039586bd372da6cd4efb810d5d66a5daeda1/regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282", size = 788038, upload-time = "2025-07-31T00:19:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7d/aabb467d8f57d8149895d133c88eb809a1a6a0fe262c1d508eb9dfabb6f9/regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588", size = 264417, upload-time = "2025-07-31T00:19:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/3b/39/bd922b55a4fc5ad5c13753274e5b536f5b06ec8eb9747675668491c7ab7a/regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62", size = 275387, upload-time = "2025-07-31T00:19:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/c61d2fdcecb754a40475a3d1ef9a000911d3e3fc75c096acf44b0dfb786a/regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176", size = 268482, upload-time = "2025-07-31T00:19:55.183Z" }, + { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334, upload-time = "2025-07-31T00:19:56.58Z" }, + { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942, upload-time = "2025-07-31T00:19:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991, upload-time = "2025-07-31T00:19:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415, upload-time = "2025-07-31T00:20:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487, upload-time = "2025-07-31T00:20:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717, upload-time = "2025-07-31T00:20:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943, upload-time = "2025-07-31T00:20:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664, upload-time = "2025-07-31T00:20:08.818Z" }, + { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457, upload-time = "2025-07-31T00:20:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008, upload-time = "2025-07-31T00:20:11.823Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101, upload-time = "2025-07-31T00:20:13.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401, upload-time = "2025-07-31T00:20:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368, upload-time = "2025-07-31T00:20:16.711Z" }, + { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482, upload-time = "2025-07-31T00:20:18.189Z" }, + { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385, upload-time = "2025-07-31T00:20:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788, upload-time = "2025-07-31T00:20:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136, upload-time = "2025-07-31T00:20:26.146Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753, upload-time = "2025-07-31T00:20:27.919Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263, upload-time = "2025-07-31T00:20:29.803Z" }, + { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103, upload-time = "2025-07-31T00:20:31.313Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709, upload-time = "2025-07-31T00:20:33.323Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726, upload-time = "2025-07-31T00:20:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306, upload-time = "2025-07-31T00:20:37.12Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494, upload-time = "2025-07-31T00:20:38.818Z" }, + { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850, upload-time = "2025-07-31T00:20:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730, upload-time = "2025-07-31T00:20:42.253Z" }, + { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640, upload-time = "2025-07-31T00:20:44.42Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757, upload-time = "2025-07-31T00:20:46.355Z" }, +] + [[package]] name = "requests" version = "2.32.3" @@ -1543,6 +2030,114 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/11/dadb85e2bd6b1f1ae56669c3e1f0410797f9605d752d68fb47b77f525b31/rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", size = 241608, upload-time = "2024-09-10T12:52:42.714Z" }, ] +[[package]] +name = "rpds-py" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/c1/49d515434c1752e40f5e35b985260cf27af052593378580a2f139a5be6b8/rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622", size = 371577, upload-time = "2025-08-07T08:23:25.379Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/bf2715b2fee5087fa13b752b5fd573f1a93e4134c74d275f709e38e54fe7/rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5", size = 354959, upload-time = "2025-08-07T08:23:26.767Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/e7762808c746dd19733a81373c10da43926f6a6adcf4920a21119697a60a/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4", size = 381485, upload-time = "2025-08-07T08:23:27.869Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/0d308eb0b558309ca0598bcba4243f52c4cd20e15fe991b5bd75824f2e61/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f", size = 396816, upload-time = "2025-08-07T08:23:29.424Z" }, + { url = "https://files.pythonhosted.org/packages/5c/aa/2d585ec911d78f66458b2c91252134ca0c7c70f687a72c87283173dc0c96/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e", size = 514950, upload-time = "2025-08-07T08:23:30.576Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ef/aced551cc1148179557aed84343073adadf252c91265263ee6203458a186/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1", size = 402132, upload-time = "2025-08-07T08:23:32.428Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/cf644803d8d417653fe2b3604186861d62ea6afaef1b2284045741baef17/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc", size = 383660, upload-time = "2025-08-07T08:23:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/caf47c55ce02b76cbaeeb2d3b36a73da9ca2e14324e3d75cf72b59dcdac5/rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85", size = 401730, upload-time = "2025-08-07T08:23:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/0b/71/c1f355afdcd5b99ffc253422aa4bdcb04ccf1491dcd1bda3688a0c07fd61/rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171", size = 416122, upload-time = "2025-08-07T08:23:36.062Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/f4b5b1eda724ed0e04d2b26d8911cdc131451a7ee4c4c020a1387e5c6ded/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d", size = 558771, upload-time = "2025-08-07T08:23:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/5f8b834db2289ab48d5cffbecbb75e35410103a77ac0b8da36bf9544ec1c/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626", size = 587876, upload-time = "2025-08-07T08:23:38.662Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/1a1df02ab8eb970115cff2ae31a6f73916609b900dc86961dc382b8c2e5e/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e", size = 554359, upload-time = "2025-08-07T08:23:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/95a014ab0d51ab6e3bebbdb476a42d992d2bbf9c489d24cff9fda998e925/rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7", size = 218084, upload-time = "2025-08-07T08:23:41.086Z" }, + { url = "https://files.pythonhosted.org/packages/49/78/f8d5b71ec65a0376b0de31efcbb5528ce17a9b7fdd19c3763303ccfdedec/rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261", size = 230085, upload-time = "2025-08-07T08:23:42.143Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/84429745184091e06b4cc70f8597408e314c2d2f7f5e13249af9ffab9e3d/rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0", size = 222112, upload-time = "2025-08-07T08:23:43.233Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, + { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, + { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, + { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, + { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, + { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" }, + { url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" }, + { url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" }, + { url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" }, + { url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" }, + { url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" }, + { url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" }, + { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, + { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" }, + { url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" }, + { url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" }, + { url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" }, + { url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" }, + { url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" }, + { url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/72ab5b911fdcc48058359b0e786e5363e3fde885156116026f1a2ba9a5b5/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089", size = 371658, upload-time = "2025-08-07T08:26:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4b/90ff04b4da055db53d8fea57640d8d5d55456343a1ec9a866c0ecfe10fd1/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d", size = 355529, upload-time = "2025-08-07T08:26:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/527491fb1afcd86fc5ce5812eb37bc70428ee017d77fee20de18155c3937/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424", size = 382822, upload-time = "2025-08-07T08:26:05.52Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a5/dcdb8725ce11e6d0913e6fcf782a13f4b8a517e8acc70946031830b98441/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8", size = 397233, upload-time = "2025-08-07T08:26:07.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/f9/0947920d1927e9f144660590cc38cadb0795d78fe0d9aae0ef71c1513b7c/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859", size = 514892, upload-time = "2025-08-07T08:26:08.622Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/d1343398c1417c68f8daa1afce56ef6ce5cc587daaf98e29347b00a80ff2/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5", size = 402733, upload-time = "2025-08-07T08:26:10.433Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0b/646f55442cd14014fb64d143428f25667a100f82092c90087b9ea7101c74/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14", size = 384447, upload-time = "2025-08-07T08:26:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/4b/15/0596ef7529828e33a6c81ecf5013d1dd33a511a3e0be0561f83079cda227/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c", size = 402502, upload-time = "2025-08-07T08:26:13.537Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8d/986af3c42f8454a6cafff8729d99fb178ae9b08a9816325ac7a8fa57c0c0/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60", size = 416651, upload-time = "2025-08-07T08:26:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9a/b4ec3629b7b447e896eec574469159b5b60b7781d3711c914748bf32de05/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be", size = 559460, upload-time = "2025-08-07T08:26:16.295Z" }, + { url = "https://files.pythonhosted.org/packages/61/63/d1e127b40c3e4733b3a6f26ae7a063cdf2bc1caa5272c89075425c7d397a/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114", size = 588072, upload-time = "2025-08-07T08:26:17.776Z" }, + { url = "https://files.pythonhosted.org/packages/04/7e/8ffc71a8f6833d9c9fb999f5b0ee736b8b159fd66968e05c7afc2dbcd57e/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466", size = 555083, upload-time = "2025-08-07T08:26:19.301Z" }, +] + [[package]] name = "ruff" version = "0.6.7" @@ -1635,14 +2230,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/36/48/4f190a83525f5cefefa44f6adc9e6386c4de5218d686c27eda92eb1f5424/sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f", size = 9562798, upload-time = "2024-09-16T20:30:05.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/61/19395d0ae78c94f6f80c8adf39a142f3fe56cfb2235d8f2317d6dae1bf0e/SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b", size = 2090086, upload-time = "2024-09-16T21:29:05.376Z" }, - { url = "https://files.pythonhosted.org/packages/e6/82/06b5fcbe5d49043e40cf4e01e3b33c471c8d9292d478420b08538cae8928/SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90", size = 2081278, upload-time = "2024-09-16T21:29:07.224Z" }, - { url = "https://files.pythonhosted.org/packages/68/d1/7fb7ee46949a5fb34005795b1fc06a8fef67587a66da731c14e545f7eb5b/SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea", size = 3063763, upload-time = "2024-09-17T01:18:12.769Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ff/a1eacd78b31e52a5073e9924fb4722ecc2a72f093ca8181ed81fc61aed2e/SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33", size = 3072032, upload-time = "2024-09-16T21:23:30.311Z" }, - { url = "https://files.pythonhosted.org/packages/21/ae/ddfecf149a6d16af87408bca7bd108eef7ef23d376cc8464317efb3cea3f/SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9", size = 3028092, upload-time = "2024-09-17T01:18:16.133Z" }, - { url = "https://files.pythonhosted.org/packages/cc/51/3e84d42121662a160bacd311cfacb29c1e6a229d59dd8edb09caa8ab283b/SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff", size = 3053543, upload-time = "2024-09-16T21:23:32.274Z" }, - { url = "https://files.pythonhosted.org/packages/3e/7a/039c78105958da3fc361887f0a82c974cb6fa5bba965c1689ec778be1c01/SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b", size = 2062372, upload-time = "2024-09-16T21:03:04.722Z" }, - { url = "https://files.pythonhosted.org/packages/a2/50/f31e927d32f9729f69d150ffe47e7cf51e3e0bb2148fc400b3e93a92ca4c/SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e", size = 2086485, upload-time = "2024-09-16T21:03:06.66Z" }, { url = "https://files.pythonhosted.org/packages/c3/46/9215a35bf98c3a2528e987791e6180eb51624d2c7d5cb8e2d96a6450b657/SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60", size = 2091274, upload-time = "2024-09-16T21:07:13.344Z" }, { url = "https://files.pythonhosted.org/packages/1e/69/919673c5101a0c633658d58b11b454b251ca82300941fba801201434755d/SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62", size = 2081672, upload-time = "2024-09-16T21:07:14.807Z" }, { url = "https://files.pythonhosted.org/packages/67/ea/a6b0597cbda12796be2302153369dbbe90573fdab3bc4885f8efac499247/SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6", size = 3200083, upload-time = "2024-09-16T22:45:15.766Z" }, @@ -1697,12 +2284,58 @@ wheels = [ ] [[package]] -name = "tomli" -version = "2.0.1" +name = "tiktoken" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/86/ad0155a37c4f310935d5ac0b1ccf9bdb635dcb906e0a9a26b616dd55825a/tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a", size = 37648, upload-time = "2025-08-08T23:58:08.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/91/912b459799a025d2842566fe1e902f7f50d54a1ce8a0f236ab36b5bd5846/tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ae374c46afadad0f501046db3da1b36cd4dfbfa52af23c998773682446097cf", size = 1059743, upload-time = "2025-08-08T23:57:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/6faa6870489ce64f5f75dcf91512bf35af5864583aee8fcb0dcb593121f5/tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25a512ff25dc6c85b58f5dd4f3d8c674dc05f96b02d66cdacf628d26a4e4866b", size = 999334, upload-time = "2025-08-08T23:57:38.595Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3e/a05d1547cf7db9dc75d1461cfa7b556a3b48e0516ec29dfc81d984a145f6/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2130127471e293d385179c1f3f9cd445070c0772be73cdafb7cec9a3684c0458", size = 1129402, upload-time = "2025-08-08T23:57:39.627Z" }, + { url = "https://files.pythonhosted.org/packages/34/9a/db7a86b829e05a01fd4daa492086f708e0a8b53952e1dbc9d380d2b03677/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e43022bf2c33f733ea9b54f6a3f6b4354b909f5a73388fb1b9347ca54a069c", size = 1184046, upload-time = "2025-08-08T23:57:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/bb/52edc8e078cf062ed749248f1454e9e5cfd09979baadb830b3940e522015/tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:adb4e308eb64380dc70fa30493e21c93475eaa11669dea313b6bbf8210bfd013", size = 1244691, upload-time = "2025-08-08T23:57:42.251Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/884b6cd7ae2570ecdcaffa02b528522b18fef1cbbfdbcaa73799807d0d3b/tiktoken-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ece6b76bfeeb61a125c44bbefdfccc279b5288e6007fbedc0d32bfec602df2f2", size = 884392, upload-time = "2025-08-08T23:57:43.628Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/eceddeffc169fc75fe0fd4f38471309f11cb1906f9b8aa39be4f5817df65/tiktoken-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fd9e6b23e860973cf9526544e220b223c60badf5b62e80a33509d6d40e6c8f5d", size = 1055199, upload-time = "2025-08-08T23:57:45.076Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/5f02bfefffdc6b54e5094d2897bc80efd43050e5b09b576fd85936ee54bf/tiktoken-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76d53cee2da71ee2731c9caa747398762bda19d7f92665e882fef229cb0b5b", size = 996655, upload-time = "2025-08-08T23:57:46.304Z" }, + { url = "https://files.pythonhosted.org/packages/65/8e/c769b45ef379bc360c9978c4f6914c79fd432400a6733a8afc7ed7b0726a/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef72aab3ea240646e642413cb363b73869fed4e604dcfd69eec63dc54d603e8", size = 1128867, upload-time = "2025-08-08T23:57:47.438Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2d/4d77f6feb9292bfdd23d5813e442b3bba883f42d0ac78ef5fdc56873f756/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f929255c705efec7a28bf515e29dc74220b2f07544a8c81b8d69e8efc4578bd", size = 1183308, upload-time = "2025-08-08T23:57:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/7a/65/7ff0a65d3bb0fc5a1fb6cc71b03e0f6e71a68c5eea230d1ff1ba3fd6df49/tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61f1d15822e4404953d499fd1dcc62817a12ae9fb1e4898033ec8fe3915fdf8e", size = 1244301, upload-time = "2025-08-08T23:57:49.642Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6e/5b71578799b72e5bdcef206a214c3ce860d999d579a3b56e74a6c8989ee2/tiktoken-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:45927a71ab6643dfd3ef57d515a5db3d199137adf551f66453be098502838b0f", size = 884282, upload-time = "2025-08-08T23:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/a9034bcee638716d9310443818d73c6387a6a96db93cbcb0819b77f5b206/tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2", size = 1055339, upload-time = "2025-08-08T23:57:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/f1/91/9922b345f611b4e92581f234e64e9661e1c524875c8eadd513c4b2088472/tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8", size = 997080, upload-time = "2025-08-08T23:57:53.442Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9d/49cd047c71336bc4b4af460ac213ec1c457da67712bde59b892e84f1859f/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4", size = 1128501, upload-time = "2025-08-08T23:57:54.808Z" }, + { url = "https://files.pythonhosted.org/packages/52/d5/a0dcdb40dd2ea357e83cb36258967f0ae96f5dd40c722d6e382ceee6bba9/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fb4effe60574675118b73c6fbfd3b5868e5d7a1f570d6cc0d18724b09ecf318", size = 1182743, upload-time = "2025-08-08T23:57:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/3b/17/a0fc51aefb66b7b5261ca1314afa83df0106b033f783f9a7bcbe8e741494/tiktoken-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94f984c9831fd32688aef4348803b0905d4ae9c432303087bae370dc1381a2b8", size = 1244057, upload-time = "2025-08-08T23:57:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/50/79/bcf350609f3a10f09fe4fc207f132085e497fdd3612f3925ab24d86a0ca0/tiktoken-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2177ffda31dec4023356a441793fed82f7af5291120751dee4d696414f54db0c", size = 883901, upload-time = "2025-08-08T23:57:59.359Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.21.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, + { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, + { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, + { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, + { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, ] [[package]] @@ -1775,7 +2408,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/01/5e637e7aa9dd031be5376b9fb749ec20b86f5a5b6a49b87fabd374d5fa9f/uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788", size = 42825, upload-time = "2024-08-13T09:27:35.098Z" } wheels = [ @@ -1799,12 +2431,6 @@ version = "0.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bc/f1/dc9577455e011ad43d9379e836ee73f40b4f99c02946849a44f7ae64835e/uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469", size = 2329938, upload-time = "2024-08-15T19:36:29.28Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/69/cc1ad125ea8ce4a4d3ba7d9836062c3fc9063cf163ddf0f168e73f3268e3/uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996", size = 1363922, upload-time = "2024-08-15T19:35:38.135Z" }, - { url = "https://files.pythonhosted.org/packages/f7/45/5a3f7a32372e4a90dfd83f30507183ec38990b8c5930ed7e36c6a15af47b/uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b", size = 760386, upload-time = "2024-08-15T19:35:39.68Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a5/9e973b25ade12c938940751bce71d0cb36efee3489014471f7d9c0a3c379/uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10", size = 3432586, upload-time = "2024-08-15T19:35:41.513Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e0/0bec8a25b2e9cf14fdfcf0229637b437c923b4e5ca22f8e988363c49bb51/uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae", size = 3431802, upload-time = "2024-08-15T19:35:43.263Z" }, - { url = "https://files.pythonhosted.org/packages/95/3b/14cef46dcec6237d858666a4a1fdb171361528c70fcd930bfc312920e7a9/uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006", size = 4144444, upload-time = "2024-08-15T19:35:45.083Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5a/0ac516562ff783f760cab3b061f10fdeb4a9f985ad4b44e7e4564ff11691/uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73", size = 4147039, upload-time = "2024-08-15T19:35:46.821Z" }, { url = "https://files.pythonhosted.org/packages/64/bf/45828beccf685b7ed9638d9b77ef382b470c6ca3b5bff78067e02ffd5663/uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037", size = 1320593, upload-time = "2024-08-15T19:35:48.431Z" }, { url = "https://files.pythonhosted.org/packages/27/c0/3c24e50bee7802a2add96ca9f0d5eb0ebab07e0a5615539d38aeb89499b9/uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9", size = 736676, upload-time = "2024-08-15T19:35:50.296Z" }, { url = "https://files.pythonhosted.org/packages/83/ce/ffa3c72954eae36825acfafd2b6a9221d79abd2670c0d25e04d6ef4a2007/uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e", size = 3494573, upload-time = "2024-08-15T19:35:52.011Z" }, @@ -1842,18 +2468,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870, upload-time = "2024-08-28T16:21:37.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/a1/631c12626378b9f1538664aa221feb5c60dfafbd7f60b451f8d0bdbcdedd/watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", size = 375096, upload-time = "2024-08-28T16:19:47.704Z" }, - { url = "https://files.pythonhosted.org/packages/f7/5c/f27c979c8a10aaa2822286c1bffdce3db731cd1aa4224b9f86623e94bbfe/watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", size = 367425, upload-time = "2024-08-28T16:19:49.66Z" }, - { url = "https://files.pythonhosted.org/packages/74/0d/1889e5649885484d29f6c792ef274454d0a26b20d6ed5fdba5409335ccb6/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", size = 437705, upload-time = "2024-08-28T16:19:51.068Z" }, - { url = "https://files.pythonhosted.org/packages/85/8a/01d9a22e839f0d1d547af11b1fcac6ba6f889513f1b2e6f221d9d60d9585/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", size = 433636, upload-time = "2024-08-28T16:19:52.799Z" }, - { url = "https://files.pythonhosted.org/packages/62/32/a93db78d340c7ef86cde469deb20e36c6b2a873edee81f610e94bbba4e06/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", size = 451069, upload-time = "2024-08-28T16:19:54.111Z" }, - { url = "https://files.pythonhosted.org/packages/99/c2/e9e2754fae3c2721c9a7736f92dab73723f1968ed72535fff29e70776008/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", size = 469306, upload-time = "2024-08-28T16:19:55.616Z" }, - { url = "https://files.pythonhosted.org/packages/4c/45/f317d9e3affb06c3c27c478de99f7110143e87f0f001f0f72e18d0e1ddce/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", size = 476187, upload-time = "2024-08-28T16:19:56.915Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d3/f1f37248abe0114916921e638f71c7d21fe77e3f2f61750e8057d0b68ef2/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", size = 425743, upload-time = "2024-08-28T16:19:57.957Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e8/c7037ea38d838fd81a59cd25761f106ee3ef2cfd3261787bee0c68908171/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", size = 612327, upload-time = "2024-08-28T16:19:59.4Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/0e6e228aafe01a7995fbfd2a4edb221bb11a2744803b65a5663fb85e5063/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", size = 595096, upload-time = "2024-08-28T16:20:01.003Z" }, - { url = "https://files.pythonhosted.org/packages/63/d5/4780e8bf3de3b4b46e7428a29654f7dc041cad6b19fd86d083e4b6f64bbe/watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", size = 264149, upload-time = "2024-08-28T16:20:02.833Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/5148898ba55fc9c111a2a4a5fb67ad3fa7eb2b3d7f0618241ed88749313d/watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", size = 277542, upload-time = "2024-08-28T16:20:03.876Z" }, { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579, upload-time = "2024-08-28T16:20:04.865Z" }, { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726, upload-time = "2024-08-28T16:20:06.111Z" }, { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735, upload-time = "2024-08-28T16:20:07.547Z" }, @@ -1892,10 +2506,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625, upload-time = "2024-08-28T16:20:50.543Z" }, { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899, upload-time = "2024-08-28T16:20:51.759Z" }, { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622, upload-time = "2024-08-28T16:20:52.82Z" }, - { url = "https://files.pythonhosted.org/packages/df/94/1ad200e937ec91b2a9d6b39ae1cf9c2b1a9cc88d5ceb43aa5c6962eb3c11/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", size = 376986, upload-time = "2024-08-28T16:21:26.895Z" }, - { url = "https://files.pythonhosted.org/packages/ee/fd/d9e020d687ccf90fe95efc513fbb39a8049cf5a3ff51f53c59fcf4c47a5d/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", size = 369445, upload-time = "2024-08-28T16:21:28.157Z" }, - { url = "https://files.pythonhosted.org/packages/43/cb/c0279b35053555d10ef03559c5aebfcb0c703d9c70a7b4e532df74b9b0e8/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", size = 439383, upload-time = "2024-08-28T16:21:29.515Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c4/08b3c2cda45db5169148a981c2100c744a4a222fa7ae7644937c0c002069/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", size = 426804, upload-time = "2024-08-28T16:21:30.687Z" }, ] [[package]] @@ -1904,17 +2514,6 @@ version = "13.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815, upload-time = "2024-09-21T17:32:27.107Z" }, - { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466, upload-time = "2024-09-21T17:32:28.428Z" }, - { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716, upload-time = "2024-09-21T17:32:29.905Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806, upload-time = "2024-09-21T17:32:31.384Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810, upload-time = "2024-09-21T17:32:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125, upload-time = "2024-09-21T17:32:33.398Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532, upload-time = "2024-09-21T17:32:35.109Z" }, - { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948, upload-time = "2024-09-21T17:32:36.214Z" }, - { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898, upload-time = "2024-09-21T17:32:37.277Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706, upload-time = "2024-09-21T17:32:38.755Z" }, - { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141, upload-time = "2024-09-21T17:32:40.495Z" }, { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813, upload-time = "2024-09-21T17:32:42.188Z" }, { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469, upload-time = "2024-09-21T17:32:43.858Z" }, { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717, upload-time = "2024-09-21T17:32:44.914Z" }, @@ -1948,12 +2547,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499, upload-time = "2024-09-21T17:33:54.917Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737, upload-time = "2024-09-21T17:33:56.052Z" }, - { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095, upload-time = "2024-09-21T17:33:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701, upload-time = "2024-09-21T17:33:59.061Z" }, - { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654, upload-time = "2024-09-21T17:34:00.944Z" }, - { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192, upload-time = "2024-09-21T17:34:02.656Z" }, { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, ] @@ -1975,17 +2568,6 @@ version = "1.17.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" }, - { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" }, - { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" }, - { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" }, - { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" }, - { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" }, { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, @@ -2041,3 +2623,94 @@ sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce wheels = [ { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, ] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, + { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From b7c95ab28b21704207f45ba90d136bd9573413cf Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:33:35 +0530 Subject: [PATCH 02/13] Resolve bot comments and keep changes in sync with main --- .../app/api/routes/doc_transformation_job.py | 8 ++-- backend/app/api/routes/documents.py | 42 ++++++++++++------- backend/app/core/doctransform/service.py | 1 + .../core/doctransform/zerox_transformer.py | 5 ++- backend/app/models/doc_transformation_job.py | 8 ++-- .../documents/test_route_document_info.py | 1 - .../documents/test_route_document_list.py | 1 - .../test_route_document_permanent_remove.py | 1 - .../documents/test_route_document_remove.py | 3 +- .../documents/test_route_document_upload.py | 2 +- .../api/routes/test_doc_transformation_job.py | 8 +--- .../test_crud_collection_create.py | 2 +- .../test_crud_collection_read_all.py | 2 +- .../tests/crud/test_doc_transformation_job.py | 6 +-- backend/app/tests/utils/document.py | 6 +-- 15 files changed, 49 insertions(+), 47 deletions(-) diff --git a/backend/app/api/routes/doc_transformation_job.py b/backend/app/api/routes/doc_transformation_job.py index 802f1fd3..707020d1 100644 --- a/backend/app/api/routes/doc_transformation_job.py +++ b/backend/app/api/routes/doc_transformation_job.py @@ -1,12 +1,12 @@ -import logging from uuid import UUID + from fastapi import APIRouter, HTTPException, Query, Path as FastPath -from app.models import DocTransformationJob, DocTransformationJobs + +from app.api.deps import CurrentUserOrgProject, SessionDep from app.crud.doc_transformation_job import DocTransformationJobCrud +from app.models import DocTransformationJob, DocTransformationJobs from app.utils import APIResponse -from app.api.deps import SessionDep, CurrentUserOrgProject -logger = logging.getLogger(__name__) router = APIRouter(prefix="/documents/transformations", tags=["doc_transformation_job"]) diff --git a/backend/app/api/routes/documents.py b/backend/app/api/routes/documents.py index b3ccbf5e..47e72fab 100644 --- a/backend/app/api/routes/documents.py +++ b/backend/app/api/routes/documents.py @@ -1,26 +1,38 @@ import logging -from uuid import UUID, uuid4 -from typing import List, Optional from pathlib import Path +from uuid import UUID, uuid4 -from fastapi import APIRouter, File, UploadFile, Query, Form, BackgroundTasks, HTTPException +from fastapi import ( + APIRouter, + BackgroundTasks, + File, + Form, + HTTPException, + Query, + UploadFile, +) from fastapi import Path as FastPath -from fastapi.responses import JSONResponse -from fastapi import HTTPException -from app.crud import DocumentCrud, CollectionCrud -from app.models import Document, DocumentPublic, Message, DocumentUploadResponse, TransformationJobInfo -from app.utils import APIResponse, load_description, get_openai_client -from app.api.deps import CurrentUser, SessionDep, CurrentUserOrgProject +from app.api.deps import CurrentUserOrgProject, SessionDep from app.core.cloud import get_cloud_storage -from app.crud.rag import OpenAIAssistantCrud from app.core.doctransform import service as transformation_service from app.core.doctransform.registry import ( - get_file_format, - is_transformation_supported, get_available_transformers, - resolve_transformer + get_file_format, + is_transformation_supported, + resolve_transformer, ) +from app.crud import CollectionCrud, DocumentCrud +from app.crud.rag import OpenAIAssistantCrud +from app.models import ( + Document, + DocumentPublic, + DocumentUploadResponse, + Message, + TransformationJobInfo, +) +from app.utils import APIResponse, get_openai_client, load_description + logger = logging.getLogger(__name__) router = APIRouter(prefix="/documents", tags=["documents"]) @@ -29,7 +41,7 @@ @router.get( "/list", description=load_description("documents/list.md"), - response_model=APIResponse[List[DocumentPublic]], + response_model=APIResponse[list[DocumentPublic]], ) def list_docs( session: SessionDep, @@ -50,8 +62,8 @@ def list_docs( async def upload_doc( session: SessionDep, current_user: CurrentUserOrgProject, + background_tasks: BackgroundTasks, src: UploadFile = File(...), - background_tasks: BackgroundTasks = None, target_format: str | None = Form( None, description="Desired output format for the uploaded document (e.g., pdf, docx, txt). " diff --git a/backend/app/core/doctransform/service.py b/backend/app/core/doctransform/service.py index a217a3f4..33c2b200 100644 --- a/backend/app/core/doctransform/service.py +++ b/backend/app/core/doctransform/service.py @@ -45,6 +45,7 @@ def execute_job( transformer_name: str, target_format: str, ): + tmp_dir: Path | None = None try: logger.info(f"[execute_job started] Transformation Job started | job_id={job_id} | transformer_name={transformer_name} | target_format={target_format} | project_id={project_id}") diff --git a/backend/app/core/doctransform/zerox_transformer.py b/backend/app/core/doctransform/zerox_transformer.py index 6a974cbd..d42b34af 100644 --- a/backend/app/core/doctransform/zerox_transformer.py +++ b/backend/app/core/doctransform/zerox_transformer.py @@ -13,7 +13,10 @@ def __init__(self, model: str = "gpt-4o"): self.model = model def transform(self, input_path: Path, output_path: Path) -> Path: - logging.info(f"ZeroxTransformer Started: {input_path} (model={self.model})") + logging.info(f"ZeroxTransformer Started: (model={self.model})") + if not input_path.exists(): + raise FileNotFoundError(f"Input file not found: {input_path}") + try: with Runner() as runner: result = runner.run(zerox( diff --git a/backend/app/models/doc_transformation_job.py b/backend/app/models/doc_transformation_job.py index d7bd86ff..26a92159 100644 --- a/backend/app/models/doc_transformation_job.py +++ b/backend/app/models/doc_transformation_job.py @@ -7,10 +7,10 @@ class TransformationStatus(str, enum.Enum): - PENDING = "pending" - PROCESSING = "processing" - COMPLETED = "completed" - FAILED = "failed" + PENDING = "PENDING" + PROCESSING = "PROCESSING" + COMPLETED = "COMPLETED" + FAILED = "FAILED" class DocTransformationJob(SQLModel, table=True): diff --git a/backend/app/tests/api/routes/documents/test_route_document_info.py b/backend/app/tests/api/routes/documents/test_route_document_info.py index 36c95493..5ed92143 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_info.py +++ b/backend/app/tests/api/routes/documents/test_route_document_info.py @@ -1,7 +1,6 @@ import pytest from sqlmodel import Session -from app.crud import get_project_by_id from app.tests.utils.document import ( DocumentComparator, DocumentMaker, diff --git a/backend/app/tests/api/routes/documents/test_route_document_list.py b/backend/app/tests/api/routes/documents/test_route_document_list.py index fc5b608c..8b2ec7a7 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_list.py +++ b/backend/app/tests/api/routes/documents/test_route_document_list.py @@ -1,7 +1,6 @@ import pytest from sqlmodel import Session -from app.crud import get_project_by_id from app.tests.utils.document import ( DocumentComparator, DocumentStore, diff --git a/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py b/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py index 46eccb23..179d247d 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py +++ b/backend/app/tests/api/routes/documents/test_route_document_permanent_remove.py @@ -12,7 +12,6 @@ import openai_responses from openai_responses import OpenAIMock -from app.crud import get_project_by_id from app.core.cloud import AmazonCloudStorageClient from app.core.config import settings from app.models import Document diff --git a/backend/app/tests/api/routes/documents/test_route_document_remove.py b/backend/app/tests/api/routes/documents/test_route_document_remove.py index b7c7f15c..7c1e3476 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_remove.py +++ b/backend/app/tests/api/routes/documents/test_route_document_remove.py @@ -1,11 +1,10 @@ import pytest import openai_responses from openai_responses import OpenAIMock -from openai import OpenAI, project +from openai import OpenAI from sqlmodel import Session, select from unittest.mock import patch -from app.crud import get_project_by_id from app.models import Document from app.tests.utils.document import ( DocumentMaker, diff --git a/backend/app/tests/api/routes/documents/test_route_document_upload.py b/backend/app/tests/api/routes/documents/test_route_document_upload.py index b5592eef..1542d579 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_upload.py +++ b/backend/app/tests/api/routes/documents/test_route_document_upload.py @@ -169,7 +169,7 @@ def test_upload_with_transformation( assert transformation_job["job_id"] == mock_job_id assert transformation_job["source_format"] == "pdf" assert transformation_job["target_format"] == "markdown" - assert transformation_job["transformer"] == "zerox" # Default transformer for pdf->markdown + assert transformation_job["transformer"] == "zerox" # Default transformer assert transformation_job["status_check_url"] == f"/documents/transformations/{mock_job_id}" assert "message" in transformation_job diff --git a/backend/app/tests/api/routes/test_doc_transformation_job.py b/backend/app/tests/api/routes/test_doc_transformation_job.py index a0e4a7b4..4c2ab0e1 100644 --- a/backend/app/tests/api/routes/test_doc_transformation_job.py +++ b/backend/app/tests/api/routes/test_doc_transformation_job.py @@ -1,16 +1,10 @@ -import pytest -from uuid import UUID from fastapi.testclient import TestClient from sqlmodel import Session from app.core.config import settings from app.crud.doc_transformation_job import DocTransformationJobCrud -from app.models import TransformationStatus +from app.models import APIKeyPublic, TransformationStatus from app.tests.utils.document import DocumentStore -from app.tests.utils.utils import get_project -from app.models import APIKeyPublic -from app.crud.project import get_project_by_id - class TestGetTransformationJob: diff --git a/backend/app/tests/crud/collections/test_crud_collection_create.py b/backend/app/tests/crud/collections/test_crud_collection_create.py index bfd54d53..53293d28 100644 --- a/backend/app/tests/crud/collections/test_crud_collection_create.py +++ b/backend/app/tests/crud/collections/test_crud_collection_create.py @@ -1,7 +1,7 @@ import openai_responses from sqlmodel import Session, select -from app.crud import CollectionCrud, get_project_by_id +from app.crud import CollectionCrud from app.models import DocumentCollection from app.tests.utils.document import DocumentStore from app.tests.utils.collection import get_collection diff --git a/backend/app/tests/crud/collections/test_crud_collection_read_all.py b/backend/app/tests/crud/collections/test_crud_collection_read_all.py index 860541e0..f8cc82fb 100644 --- a/backend/app/tests/crud/collections/test_crud_collection_read_all.py +++ b/backend/app/tests/crud/collections/test_crud_collection_read_all.py @@ -3,7 +3,7 @@ from openai import OpenAI from sqlmodel import Session -from app.crud import CollectionCrud, get_project_by_id +from app.crud import CollectionCrud from app.models import Collection from app.tests.utils.document import DocumentStore from app.tests.utils.collection import get_collection diff --git a/backend/app/tests/crud/test_doc_transformation_job.py b/backend/app/tests/crud/test_doc_transformation_job.py index 602e0fd6..01edfbee 100644 --- a/backend/app/tests/crud/test_doc_transformation_job.py +++ b/backend/app/tests/crud/test_doc_transformation_job.py @@ -1,11 +1,11 @@ import pytest -from uuid import UUID from sqlmodel import Session from app.crud.doc_transformation_job import DocTransformationJobCrud -from app.models import DocTransformationJob, TransformationStatus +from app.models import TransformationStatus from app.core.exception_handlers import HTTPException from app.tests.utils.document import DocumentStore from app.tests.utils.utils import get_project, SequentialUuidGenerator +from app.tests.utils.test_data import create_test_project @pytest.fixture @@ -104,7 +104,7 @@ def test_cannot_read_job_from_different_project(self, db: Session, store: Docume job = job_crud.create(document.id) # Try to read from different project - other_project = get_project(db, name="Dalgo") + other_project = create_test_project(db) other_crud = DocTransformationJobCrud(db, other_project.id) with pytest.raises(HTTPException) as exc_info: diff --git a/backend/app/tests/utils/document.py b/backend/app/tests/utils/document.py index efa7dce0..674035fc 100644 --- a/backend/app/tests/utils/document.py +++ b/backend/app/tests/utils/document.py @@ -32,12 +32,8 @@ def __init__(self, project_id: int, session: Session): def __iter__(self): return self - + def __next__(self): - if self.project is None: - self.project = get_project_by_id( - session=self.session, project_id=self.project_id - ) doc_id = next(self.index) key = f"{self.project.storage_path}/{doc_id}.txt" From 9fd7ade2c19c417148430921e80050ee43cab9d0 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:34:08 +0530 Subject: [PATCH 03/13] pre commit --- ...6fd_create_doc_transformation_job_table.py | 45 +++-- ...dd_source_document_id_to_document_table.py | 12 +- backend/app/api/docs/documents/upload.md | 4 +- .../app/api/routes/doc_transformation_job.py | 8 +- backend/app/api/routes/documents.py | 37 ++-- backend/app/core/doctransform/registry.py | 36 +++- backend/app/core/doctransform/service.py | 47 +++-- .../app/core/doctransform/test_transformer.py | 3 +- backend/app/core/doctransform/transformer.py | 1 + .../core/doctransform/zerox_transformer.py | 24 ++- backend/app/crud/doc_transformation_job.py | 19 +- backend/app/models/__init__.py | 13 +- backend/app/models/doc_transformation_job.py | 6 +- backend/app/models/document.py | 25 +-- .../documents/test_route_document_upload.py | 85 +++++--- .../api/routes/test_doc_transformation_job.py | 183 +++++++----------- .../core/doctransformer/test_service/base.py | 69 +++---- .../doctransformer/test_service/conftest.py | 24 ++- .../test_service/test_execute_job.py | 107 +++++----- .../test_service/test_execute_job_errors.py | 117 ++++++----- .../test_service/test_integration.py | 50 ++--- .../test_service/test_start_job.py | 87 +++++---- .../tests/crud/test_doc_transformation_job.py | 144 ++++++++------ backend/app/tests/utils/document.py | 5 +- 24 files changed, 652 insertions(+), 499 deletions(-) diff --git a/backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py b/backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py index dec2d783..822c2d87 100644 --- a/backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py +++ b/backend/app/alembic/versions/9f8a4af9d6fd_create_doc_transformation_job_table.py @@ -11,30 +11,47 @@ # revision identifiers, used by Alembic. -revision = '9f8a4af9d6fd' -down_revision = 'b5b9412d3d2a' +revision = "9f8a4af9d6fd" +down_revision = "b5b9412d3d2a" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('doc_transformation_job', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('source_document_id', sa.Uuid(), nullable=False), - sa.Column('transformed_document_id', sa.Uuid(), nullable=True), - sa.Column('status', sa.Enum('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', name='transformationstatus'), nullable=False), - sa.Column('error_message', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['source_document_id'], ['document.id'], ), - sa.ForeignKeyConstraint(['transformed_document_id'], ['document.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "doc_transformation_job", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("source_document_id", sa.Uuid(), nullable=False), + sa.Column("transformed_document_id", sa.Uuid(), nullable=True), + sa.Column( + "status", + sa.Enum( + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED", + name="transformationstatus", + ), + nullable=False, + ), + sa.Column("error_message", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["source_document_id"], + ["document.id"], + ), + sa.ForeignKeyConstraint( + ["transformed_document_id"], + ["document.id"], + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('doc_transformation_job') + op.drop_table("doc_transformation_job") # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py b/backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py index 8220d918..d0eec5ed 100644 --- a/backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py +++ b/backend/app/alembic/versions/b5b9412d3d2a_add_source_document_id_to_document_table.py @@ -11,21 +11,21 @@ # revision identifiers, used by Alembic. -revision = 'b5b9412d3d2a' -down_revision = '40307ab77e9f' +revision = "b5b9412d3d2a" +down_revision = "40307ab77e9f" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('document', sa.Column('source_document_id', sa.Uuid(), nullable=True)) - op.create_foreign_key(None, 'document', 'document', ['source_document_id'], ['id']) + op.add_column("document", sa.Column("source_document_id", sa.Uuid(), nullable=True)) + op.create_foreign_key(None, "document", "document", ["source_document_id"], ["id"]) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'document', type_='foreignkey') - op.drop_column('document', 'source_document_id') + op.drop_constraint(None, "document", type_="foreignkey") + op.drop_column("document", "source_document_id") # ### end Alembic commands ### diff --git a/backend/app/api/docs/documents/upload.md b/backend/app/api/docs/documents/upload.md index a6d8bd42..87e46d47 100644 --- a/backend/app/api/docs/documents/upload.md +++ b/backend/app/api/docs/documents/upload.md @@ -8,10 +8,10 @@ Upload a document to the AI platform. The following (source_format → target_format) transformations are supported: - pdf → markdown - - zerox + - zerox ### Transformers Available transformer names and their implementations, default transformer is zerox: -- `zerox` \ No newline at end of file +- `zerox` diff --git a/backend/app/api/routes/doc_transformation_job.py b/backend/app/api/routes/doc_transformation_job.py index 707020d1..c66ad910 100644 --- a/backend/app/api/routes/doc_transformation_job.py +++ b/backend/app/api/routes/doc_transformation_job.py @@ -33,7 +33,9 @@ def get_transformation_job( def get_multiple_transformation_jobs( session: SessionDep, current_user: CurrentUserOrgProject, - job_ids: str = Query(..., description="Comma-separated list of transformation job IDs"), + job_ids: str = Query( + ..., description="Comma-separated list of transformation job IDs" + ), ): job_id_list = [] invalid_ids = [] @@ -55,4 +57,6 @@ def get_multiple_transformation_jobs( crud = DocTransformationJobCrud(session, project_id=current_user.project_id) jobs = crud.read_each(set(job_id_list)) jobs_not_found = set(job_id_list) - {job.id for job in jobs} - return APIResponse.success_response(DocTransformationJobs(jobs=jobs, jobs_not_found=jobs_not_found)) \ No newline at end of file + return APIResponse.success_response( + DocTransformationJobs(jobs=jobs, jobs_not_found=jobs_not_found) + ) diff --git a/backend/app/api/routes/documents.py b/backend/app/api/routes/documents.py index 47e72fab..e95c0c9e 100644 --- a/backend/app/api/routes/documents.py +++ b/backend/app/api/routes/documents.py @@ -64,13 +64,14 @@ async def upload_doc( current_user: CurrentUserOrgProject, background_tasks: BackgroundTasks, src: UploadFile = File(...), - target_format: str | None = Form( + target_format: str + | None = Form( None, - description="Desired output format for the uploaded document (e.g., pdf, docx, txt). " + description="Desired output format for the uploaded document (e.g., pdf, docx, txt). ", ), - transformer: str | None = Form( - None, - description="Name of the transformer to apply when converting. " + transformer: str + | None = Form( + None, description="Name of the transformer to apply when converting. " ), ): # Determine source file format @@ -84,19 +85,23 @@ async def upload_doc( if not is_transformation_supported(source_format, target_format): raise HTTPException( status_code=400, - detail=f"Transformation from {source_format} to {target_format} is not supported" + detail=f"Transformation from {source_format} to {target_format} is not supported", ) # Resolve the transformer to use if not transformer: transformer = "default" try: - actual_transformer = resolve_transformer(source_format, target_format, transformer) + actual_transformer = resolve_transformer( + source_format, target_format, transformer + ) except ValueError as e: - available_transformers = get_available_transformers(source_format, target_format) + available_transformers = get_available_transformers( + source_format, target_format + ) raise HTTPException( status_code=400, - detail=f"{str(e)}. Available transformers: {list(available_transformers.keys())}" + detail=f"{str(e)}. Available transformers: {list(available_transformers.keys())}", ) storage = get_cloud_storage(session=session, project_id=current_user.project_id) @@ -112,7 +117,6 @@ async def upload_doc( ) source_document = crud.update(document) - job_info: TransformationJobInfo | None = None if target_format and actual_transformer: job_id = transformation_service.start_job( @@ -129,14 +133,17 @@ async def upload_doc( source_format=source_format, target_format=target_format, transformer=actual_transformer, - status_check_url=f"/documents/transformations/{job_id}" + status_check_url=f"/documents/transformations/{job_id}", ) - document_schema = DocumentPublic.model_validate(source_document, from_attributes=True) - document_schema.signed_url = storage.get_signed_url(source_document.object_store_url) + document_schema = DocumentPublic.model_validate( + source_document, from_attributes=True + ) + document_schema.signed_url = storage.get_signed_url( + source_document.object_store_url + ) response = DocumentUploadResponse( - **document_schema.model_dump(), - transformation_job=job_info + **document_schema.model_dump(), transformation_job=job_info ) return APIResponse.success_response(response) diff --git a/backend/app/core/doctransform/registry.py b/backend/app/core/doctransform/registry.py index 51f94944..e75f2247 100644 --- a/backend/app/core/doctransform/registry.py +++ b/backend/app/core/doctransform/registry.py @@ -5,9 +5,11 @@ from .test_transformer import TestTransformer from .zerox_transformer import ZeroxTransformer + class TransformationError(Exception): """Raised when a document transformation fails.""" + # Map transformer names to their classes TRANSFORMERS: Dict[str, Type[Transformer]] = { "default": ZeroxTransformer, @@ -30,7 +32,7 @@ class TransformationError(Exception): EXTENSION_TO_FORMAT: Dict[str, str] = { ".pdf": "pdf", ".docx": "docx", - ".doc": "doc", + ".doc": "doc", ".html": "html", ".htm": "html", ".txt": "text", @@ -48,6 +50,7 @@ class TransformationError(Exception): "markdown": ".md", } + def get_file_format(filename: str) -> str: """Extract format from filename extension.""" ext = Path(filename).suffix.lower() @@ -56,46 +59,57 @@ def get_file_format(filename: str) -> str: raise ValueError(f"Unsupported file extension: {ext}") return format_name + def get_supported_transformations() -> Dict[Tuple[str, str], Set[str]]: """Get all supported transformation combinations.""" return { - key: set(transformers.keys()) + key: set(transformers.keys()) for key, transformers in SUPPORTED_TRANSFORMATIONS.items() } + def is_transformation_supported(source_format: str, target_format: str) -> bool: """Check if a transformation from source_format to target_format is supported.""" return (source_format, target_format) in SUPPORTED_TRANSFORMATIONS -def get_available_transformers(source_format: str, target_format: str) -> Dict[str, str]: + +def get_available_transformers( + source_format: str, target_format: str +) -> Dict[str, str]: """Get available transformers for a specific transformation.""" return SUPPORTED_TRANSFORMATIONS.get((source_format, target_format), {}) -def resolve_transformer(source_format: str, target_format: str, transformer_name: Optional[str] = None) -> str: + +def resolve_transformer( + source_format: str, target_format: str, transformer_name: Optional[str] = None +) -> str: """ Resolve the actual transformer to use for a transformation. Returns the transformer name to use. """ available_transformers = get_available_transformers(source_format, target_format) - + if not available_transformers: raise ValueError( f"Transformation from {source_format} to {target_format} is not supported" ) - + if transformer_name is None: transformer_name = "default" - + if transformer_name not in available_transformers: available = ", ".join(available_transformers.keys()) raise ValueError( f"Transformer '{transformer_name}' not available for {source_format} to {target_format}. " f"Available: {available}" ) - + return available_transformers[transformer_name] -def convert_document(input_path: Path, output_path: Path, transformer_name: str = "default") -> Path: + +def convert_document( + input_path: Path, output_path: Path, transformer_name: str = "default" +) -> Path: """ Select and run the specified transformer on the input_path, writing to output_path. Returns the path to the transformed file. @@ -104,7 +118,9 @@ def convert_document(input_path: Path, output_path: Path, transformer_name: str transformer_cls = TRANSFORMERS[transformer_name] except KeyError: available = ", ".join(TRANSFORMERS.keys()) - raise ValueError(f"Transformer '{transformer_name}' not found. Available: {available}") + raise ValueError( + f"Transformer '{transformer_name}' not found. Available: {available}" + ) transformer = transformer_cls() try: diff --git a/backend/app/core/doctransform/service.py b/backend/app/core/doctransform/service.py index 33c2b200..2e3f2792 100644 --- a/backend/app/core/doctransform/service.py +++ b/backend/app/core/doctransform/service.py @@ -21,6 +21,7 @@ logger = logging.getLogger(__name__) + def start_job( db: Session, current_user: CurrentUserOrgProject, @@ -31,13 +32,18 @@ def start_job( ) -> UUID: job_crud = DocTransformationJobCrud(session=db, project_id=current_user.project_id) job = job_crud.create(source_document_id=source_document_id) - + # Extract the project ID before passing to background task project_id = current_user.project_id - background_tasks.add_task(execute_job, project_id, job.id, transformer_name, target_format) - logger.info(f"[start_job] Job scheduled for document transformation | id: {job.id}, project_id: {project_id}") + background_tasks.add_task( + execute_job, project_id, job.id, transformer_name, target_format + ) + logger.info( + f"[start_job] Job scheduled for document transformation | id: {job.id}, project_id: {project_id}" + ) return job.id + @retry(wait=wait_exponential(multiplier=5, min=5, max=10), stop=stop_after_attempt(3)) def execute_job( project_id: int, @@ -47,7 +53,9 @@ def execute_job( ): tmp_dir: Path | None = None try: - logger.info(f"[execute_job started] Transformation Job started | job_id={job_id} | transformer_name={transformer_name} | target_format={target_format} | project_id={project_id}") + logger.info( + f"[execute_job started] Transformation Job started | job_id={job_id} | transformer_name={transformer_name} | target_format={target_format} | project_id={project_id}" + ) # Update job status to PROCESSING and fetch source document info with Session(engine) as db: @@ -55,9 +63,9 @@ def execute_job( job = job_crud.update_status(job_id, TransformationStatus.PROCESSING) doc_crud = DocumentCrud(session=db, project_id=project_id) - + source_doc = doc_crud.read_one(job.source_document_id) - + source_doc_id = source_doc.id source_doc_fname = source_doc.fname source_doc_object_store_url = source_doc.object_store_url @@ -88,7 +96,6 @@ def execute_job( } content_type = content_type_map.get(target_format, "text/plain") - # upload transformed file and create document record with open(tmp_out, "rb") as fobj: file_upload = UploadFile( @@ -110,19 +117,33 @@ def execute_job( created = DocumentCrud(db, project_id).update(new_doc) job_crud = DocTransformationJobCrud(session=db, project_id=project_id) - job_crud.update_status(job_id, TransformationStatus.COMPLETED, transformed_document_id=created.id) + job_crud.update_status( + job_id, + TransformationStatus.COMPLETED, + transformed_document_id=created.id, + ) - logger.info(f"[execute_job] Doc Transformation job completed | job_id={job_id} | transformed_doc_id={created.id} | project_id={project_id}") + logger.info( + f"[execute_job] Doc Transformation job completed | job_id={job_id} | transformed_doc_id={created.id} | project_id={project_id}" + ) except Exception as e: - logger.error(f"Transformation job failed | job_id={job_id} | error={e}", exc_info=True) + logger.error( + f"Transformation job failed | job_id={job_id} | error={e}", exc_info=True + ) try: with Session(engine) as db: job_crud = DocTransformationJobCrud(session=db, project_id=project_id) - job_crud.update_status(job_id, TransformationStatus.FAILED, error_message=str(e)) - logger.info(f"[execute_job] Doc Transformation job failed | job_id={job_id} | error={e}") + job_crud.update_status( + job_id, TransformationStatus.FAILED, error_message=str(e) + ) + logger.info( + f"[execute_job] Doc Transformation job failed | job_id={job_id} | error={e}" + ) except Exception as db_error: - logger.error(f"Failed to update job status to FAILED | job_id={job_id} | db_error={db_error}") + logger.error( + f"Failed to update job status to FAILED | job_id={job_id} | db_error={db_error}" + ) raise finally: if tmp_dir and tmp_dir.exists(): diff --git a/backend/app/core/doctransform/test_transformer.py b/backend/app/core/doctransform/test_transformer.py index 66d5ba32..6f3b274b 100644 --- a/backend/app/core/doctransform/test_transformer.py +++ b/backend/app/core/doctransform/test_transformer.py @@ -1,6 +1,7 @@ from pathlib import Path from .transformer import Transformer + class TestTransformer(Transformer): """ A test transformer that returns a hardcoded lorem ipsum string. @@ -11,5 +12,5 @@ def transform(self, input_path: Path, output_path: Path) -> Path: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." ) - output_path.write_text(content, encoding='utf-8') + output_path.write_text(content, encoding="utf-8") return output_path diff --git a/backend/app/core/doctransform/transformer.py b/backend/app/core/doctransform/transformer.py index f487aaed..ef4babea 100644 --- a/backend/app/core/doctransform/transformer.py +++ b/backend/app/core/doctransform/transformer.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from pathlib import Path + class Transformer(ABC): """Abstract base for document transformers.""" diff --git a/backend/app/core/doctransform/zerox_transformer.py b/backend/app/core/doctransform/zerox_transformer.py index d42b34af..2c65253f 100644 --- a/backend/app/core/doctransform/zerox_transformer.py +++ b/backend/app/core/doctransform/zerox_transformer.py @@ -4,6 +4,7 @@ from .transformer import Transformer from pyzerox import zerox + class ZeroxTransformer(Transformer): """ Transformer that uses zerox to extract content from PDFs. @@ -19,30 +20,35 @@ def transform(self, input_path: Path, output_path: Path) -> Path: try: with Runner() as runner: - result = runner.run(zerox( - file_path=str(input_path), - model=self.model, - )) + result = runner.run( + zerox( + file_path=str(input_path), + model=self.model, + ) + ) if result is None or not hasattr(result, "pages") or result.pages is None: - raise RuntimeError("Zerox returned no pages. This may indicate a PDF/image conversion failure (is Poppler installed and in PATH?)") + raise RuntimeError( + "Zerox returned no pages. This may indicate a PDF/image conversion failure (is Poppler installed and in PATH?)" + ) with output_path.open("w", encoding="utf-8") as output_file: for page in result.pages: if not getattr(page, "content", None): - continue + continue output_file.write(page.content) output_file.write("\n\n") - logging.info(f"[ZeroxTransformer.transform] Transformation completed, output written to: {output_path}") + logging.info( + f"[ZeroxTransformer.transform] Transformation completed, output written to: {output_path}" + ) return output_path except Exception as e: logging.error( f"ZeroxTransformer failed for {input_path}: {e}\n" "This may be due to a missing Poppler installation or a corrupt PDF file.", - exc_info=True + exc_info=True, ) raise RuntimeError( f"Failed to extract content from PDF. " f"Check that Poppler is installed and in your PATH. Original error: {e}" ) from e - diff --git a/backend/app/crud/doc_transformation_job.py b/backend/app/crud/doc_transformation_job.py index 4a84cd43..7edf5f8d 100644 --- a/backend/app/crud/doc_transformation_job.py +++ b/backend/app/crud/doc_transformation_job.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) + class DocTransformationJobCrud: def __init__(self, session: Session, project_id: int): self.session = session @@ -23,7 +24,9 @@ def create(self, source_document_id: UUID) -> DocTransformationJob: self.session.add(job) self.session.commit() self.session.refresh(job) - logger.info(f"[DocTransformationJobCrud.create] Created new transformation job | id: {job.id}, source_document_id: {source_document_id}") + logger.info( + f"[DocTransformationJobCrud.create] Created new transformation job | id: {job.id}, source_document_id: {source_document_id}" + ) return job def read_one(self, job_id: UUID) -> DocTransformationJob: @@ -34,14 +37,16 @@ def read_one(self, job_id: UUID) -> DocTransformationJob: and_( DocTransformationJob.id == job_id, Document.project_id == self.project_id, - Document.is_deleted.is_(False) + Document.is_deleted.is_(False), ) ) ) - + job = self.session.exec(statement).one_or_none() if not job: - logger.warning(f"[DocTransformationJobCrud.read_one] Job not found or Document is deleted | id: {job_id}, project_id: {self.project_id}") + logger.warning( + f"[DocTransformationJobCrud.read_one] Job not found or Document is deleted | id: {job_id}, project_id: {self.project_id}" + ) raise HTTPException(status_code=404, detail="Transformation job not found") return job @@ -53,7 +58,7 @@ def read_each(self, job_ids: set[UUID]) -> list[DocTransformationJob]: and_( DocTransformationJob.id.in_(list(job_ids)), Document.project_id == self.project_id, - Document.is_deleted.is_(False) + Document.is_deleted.is_(False), ) ) ) @@ -80,5 +85,7 @@ def update_status( self.session.add(job) self.session.commit() self.session.refresh(job) - logger.info(f"[DocTransformationJobCrud.update_status] Updated job status | id: {job.id}, status: {status}") + logger.info( + f"[DocTransformationJobCrud.update_status] Updated job status | id: {job.id}, status: {status}" + ) return job diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 82639861..977ff667 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -2,8 +2,17 @@ from .auth import Token, TokenPayload from .collection import Collection -from .document import Document, DocumentPublic, DocumentUploadResponse, TransformationJobInfo -from .doc_transformation_job import DocTransformationJob, DocTransformationJobs, TransformationStatus +from .document import ( + Document, + DocumentPublic, + DocumentUploadResponse, + TransformationJobInfo, +) +from .doc_transformation_job import ( + DocTransformationJob, + DocTransformationJobs, + TransformationStatus, +) from .document_collection import DocumentCollection from .message import Message diff --git a/backend/app/models/doc_transformation_job.py b/backend/app/models/doc_transformation_job.py index 26a92159..b968d883 100644 --- a/backend/app/models/doc_transformation_job.py +++ b/backend/app/models/doc_transformation_job.py @@ -18,7 +18,9 @@ class DocTransformationJob(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) source_document_id: UUID = Field(foreign_key="document.id") - transformed_document_id: Optional[UUID] = Field(default=None, foreign_key="document.id") + transformed_document_id: Optional[UUID] = Field( + default=None, foreign_key="document.id" + ) status: TransformationStatus = Field(default=TransformationStatus.PENDING) error_message: Optional[str] = Field(default=None) created_at: datetime = Field(default_factory=now) @@ -27,4 +29,4 @@ class DocTransformationJob(SQLModel, table=True): class DocTransformationJobs(SQLModel): jobs: list[DocTransformationJob] - jobs_not_found: list[UUID] \ No newline at end of file + jobs_not_found: list[UUID] diff --git a/backend/app/models/document.py b/backend/app/models/document.py index 2f44aeb0..16d77a17 100644 --- a/backend/app/models/document.py +++ b/backend/app/models/document.py @@ -53,35 +53,24 @@ class DocumentPublic(DocumentBase): ) source_document_id: UUID | None = Field( default=None, - description="The ID of the source document if this document is a transformation" + description="The ID of the source document if this document is a transformation", ) signed_url: str | None = Field( - default=None, - description="A signed URL for accessing the document" + default=None, description="A signed URL for accessing the document" ) class TransformationJobInfo(SQLModel): message: str - job_id: UUID = Field( - description="The unique identifier of the transformation job" - ) - source_format: str = Field( - description="The format of the source document" - ) - target_format: str = Field( - description="The format of the target document" - ) - transformer: str = Field( - description="The name of the transformer used" - ) + job_id: UUID = Field(description="The unique identifier of the transformation job") + source_format: str = Field(description="The format of the source document") + target_format: str = Field(description="The format of the target document") + transformer: str = Field(description="The name of the transformer used") status_check_url: str = Field( description="The URL to check the status of the transformation job" ) class DocumentUploadResponse(DocumentPublic): - signed_url: str = Field( - description="A signed URL for accessing the document" - ) + signed_url: str = Field(description="A signed URL for accessing the document") transformation_job: TransformationJobInfo | None = None diff --git a/backend/app/tests/api/routes/documents/test_route_document_upload.py b/backend/app/tests/api/routes/documents/test_route_document_upload.py index 1542d579..4b73a451 100644 --- a/backend/app/tests/api/routes/documents/test_route_document_upload.py +++ b/backend/app/tests/api/routes/documents/test_route_document_upload.py @@ -22,16 +22,22 @@ class WebUploader(WebCrawler): - def put(self, route: Route, scratch: Path, target_format: str = None, transformer: str = None): + def put( + self, + route: Route, + scratch: Path, + target_format: str = None, + transformer: str = None, + ): (mtype, _) = mimetypes.guess_type(str(scratch)) files = {"src": (str(scratch), scratch.open("rb"), mtype)} - + data = {} if target_format: data["target_format"] = target_format if transformer: data["transformer"] = transformer - + return self.client.post( str(route), headers={"X-API-KEY": self.user_api_key.key}, @@ -55,7 +61,9 @@ def pdf_scratch(): fp.write("2 0 obj<>endobj\n") fp.write("3 0 obj<>endobj\n") fp.write("xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n") - fp.write("0000000074 00000 n \n0000000120 00000 n \ntrailer<>\n") + fp.write( + "0000000074 00000 n \n0000000120 00000 n \ntrailer<>\n" + ) fp.write("startxref\n202\n%%EOF") fp.flush() yield Path(fp.name) @@ -137,13 +145,13 @@ def test_upload_without_transformation( aws.create() response = httpx_to_standard(uploader.put(route, scratch)) - + assert response.success is True assert response.data["transformation_job"] is None assert "id" in response.data assert "fname" in response.data - @patch('app.core.doctransform.service.start_job') + @patch("app.core.doctransform.service.start_job") def test_upload_with_transformation( self, mock_start_job, @@ -155,25 +163,30 @@ def test_upload_with_transformation( """Test upload with valid transformation parameters.""" aws = AmazonCloudStorageClient() aws.create() - + # Mock the background job creation mock_job_id = "12345678-1234-5678-9abc-123456789012" mock_start_job.return_value = mock_job_id - response = httpx_to_standard(uploader.put(route, pdf_scratch, target_format="markdown")) - + response = httpx_to_standard( + uploader.put(route, pdf_scratch, target_format="markdown") + ) + assert response.success is True assert response.data["transformation_job"] is not None - + transformation_job = response.data["transformation_job"] assert transformation_job["job_id"] == mock_job_id assert transformation_job["source_format"] == "pdf" assert transformation_job["target_format"] == "markdown" assert transformation_job["transformer"] == "zerox" # Default transformer - assert transformation_job["status_check_url"] == f"/documents/transformations/{mock_job_id}" + assert ( + transformation_job["status_check_url"] + == f"/documents/transformations/{mock_job_id}" + ) assert "message" in transformation_job - @patch('app.core.doctransform.service.start_job') + @patch("app.core.doctransform.service.start_job") def test_upload_with_specific_transformer( self, mock_start_job, @@ -185,14 +198,16 @@ def test_upload_with_specific_transformer( """Test upload with specific transformer specified.""" aws = AmazonCloudStorageClient() aws.create() - + mock_job_id = "12345678-1234-5678-9abc-123456789012" mock_start_job.return_value = mock_job_id - response = httpx_to_standard(uploader.put( - route, pdf_scratch, target_format="markdown", transformer="zerox" - )) - + response = httpx_to_standard( + uploader.put( + route, pdf_scratch, target_format="markdown", transformer="zerox" + ) + ) + assert response.success is True transformation_job = response.data["transformation_job"] assert transformation_job["transformer"] == "zerox" @@ -209,7 +224,7 @@ def test_upload_with_unsupported_transformation( aws.create() response = uploader.put(route, scratch, target_format="pdf") - + assert response.status_code == 400 error_data = response.json() assert "Transformation from text to pdf is not supported" in error_data["error"] @@ -226,9 +241,12 @@ def test_upload_with_invalid_transformer( aws.create() response = uploader.put( - route, pdf_scratch, target_format="markdown", transformer="invalid_transformer" + route, + pdf_scratch, + target_format="markdown", + transformer="invalid_transformer", ) - + assert response.status_code == 400 error_data = response.json() assert "Transformer 'invalid_transformer' not available" in error_data["error"] @@ -258,7 +276,7 @@ def test_upload_with_unsupported_file_extension( finally: unsupported_file.unlink() - @patch('app.core.doctransform.service.start_job') + @patch("app.core.doctransform.service.start_job") def test_transformation_job_created_in_database( self, mock_start_job, @@ -270,17 +288,19 @@ def test_transformation_job_created_in_database( """Test that transformation job is properly stored in the database.""" aws = AmazonCloudStorageClient() aws.create() - + mock_job_id = "12345678-1234-5678-9abc-123456789012" mock_start_job.return_value = mock_job_id - response = httpx_to_standard(uploader.put(route, pdf_scratch, target_format="markdown")) - + response = httpx_to_standard( + uploader.put(route, pdf_scratch, target_format="markdown") + ) + mock_start_job.assert_called_once() args, kwargs = mock_start_job.call_args - + # Check that start_job was called with the right arguments - assert 'transformer_name' in kwargs or len(args) >= 4 + assert "transformer_name" in kwargs or len(args) >= 4 def test_upload_response_structure_without_transformation( self, @@ -294,9 +314,16 @@ def test_upload_response_structure_without_transformation( aws.create() response = httpx_to_standard(uploader.put(route, scratch)) - - required_fields = ["id", "project_id", "fname", "inserted_at", "updated_at", "source_document_id"] + + required_fields = [ + "id", + "project_id", + "fname", + "inserted_at", + "updated_at", + "source_document_id", + ] for field in required_fields: assert field in response.data - + assert response.data["transformation_job"] is None diff --git a/backend/app/tests/api/routes/test_doc_transformation_job.py b/backend/app/tests/api/routes/test_doc_transformation_job.py index 4c2ab0e1..7eeb7dee 100644 --- a/backend/app/tests/api/routes/test_doc_transformation_job.py +++ b/backend/app/tests/api/routes/test_doc_transformation_job.py @@ -9,22 +9,18 @@ class TestGetTransformationJob: def test_get_existing_job_success( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test successfully retrieving an existing transformation job.""" document = DocumentStore(db, user_api_key.project_id).put() job = DocTransformationJobCrud(db, user_api_key.project_id) created_job = job.create(document.id) - response = client.get( f"{settings.API_V1_STR}/documents/transformations/{created_job.id}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert "data" in data @@ -35,34 +31,29 @@ def test_get_existing_job_success( assert data["data"]["transformed_document_id"] is None def test_get_nonexistent_job_404( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test getting a non-existent transformation job returns 404.""" fake_uuid = "00000000-0000-0000-0000-000000000001" - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/{fake_uuid}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 404 def test_get_job_invalid_uuid_422( - self, - client: TestClient, - user_api_key: APIKeyPublic + self, client: TestClient, user_api_key: APIKeyPublic ): """Test getting a job with invalid UUID format returns 422.""" invalid_uuid = "not-a-uuid" - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/{invalid_uuid}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 422 def test_get_job_different_project_404( @@ -70,27 +61,24 @@ def test_get_job_different_project_404( client: TestClient, db: Session, user_api_key: APIKeyPublic, - superuser_api_key: APIKeyPublic + superuser_api_key: APIKeyPublic, ): """Test that jobs from different projects are not accessible.""" store = DocumentStore(db, user_api_key.project_id) crud = DocTransformationJobCrud(db, user_api_key.project_id) document = store.put() job = crud.create(document.id) - + # Try to access with user from different project (superuser) response = client.get( f"{settings.API_V1_STR}/documents/transformations/{job.id}", - headers={"X-API-KEY": superuser_api_key.key} + headers={"X-API-KEY": superuser_api_key.key}, ) - + assert response.status_code == 404 def test_get_completed_job_with_result( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test getting a completed job with transformation result.""" store = DocumentStore(db, user_api_key.project_id) @@ -98,29 +86,26 @@ def test_get_completed_job_with_result( source_document = store.put() transformed_document = store.put() job = crud.create(source_document.id) - + # Update job to completed status crud.update_status( job.id, TransformationStatus.COMPLETED, - transformed_document_id=transformed_document.id + transformed_document_id=transformed_document.id, ) - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/{job.id}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert data["data"]["status"] == TransformationStatus.COMPLETED assert data["data"]["transformed_document_id"] == str(transformed_document.id) def test_get_failed_job_with_error( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test getting a failed job with error message.""" store = DocumentStore(db, user_api_key.project_id) @@ -128,19 +113,15 @@ def test_get_failed_job_with_error( document = store.put() job = crud.create(document.id) error_msg = "Transformation failed due to invalid format" - + # Update job to failed status - crud.update_status( - job.id, - TransformationStatus.FAILED, - error_message=error_msg - ) - + crud.update_status(job.id, TransformationStatus.FAILED, error_message=error_msg) + response = client.get( f"{settings.API_V1_STR}/documents/transformations/{job.id}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert data["data"]["status"] == TransformationStatus.FAILED @@ -149,10 +130,7 @@ def test_get_failed_job_with_error( class TestGetMultipleTransformationJobs: def test_get_multiple_jobs_success( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test successfully retrieving multiple transformation jobs.""" store = DocumentStore(db, user_api_key.project_id) @@ -160,27 +138,24 @@ def test_get_multiple_jobs_success( documents = store.fill(3) jobs = [crud.create(doc.id) for doc in documents] job_ids_str = ",".join(str(job.id) for job in jobs) - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert "data" in data assert len(data["data"]["jobs"]) == 3 assert len(data["data"]["jobs_not_found"]) == 0 - + returned_ids = {job["id"] for job in data["data"]["jobs"]} expected_ids = {str(job.id) for job in jobs} assert returned_ids == expected_ids def test_get_mixed_existing_nonexisting_jobs( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test retrieving a mix of existing and non-existing jobs.""" store = DocumentStore(db, user_api_key.project_id) @@ -188,14 +163,14 @@ def test_get_mixed_existing_nonexisting_jobs( documents = store.fill(2) jobs = [crud.create(doc.id) for doc in documents] fake_uuid = "00000000-0000-0000-0000-000000000001" - + job_ids_str = f"{jobs[0].id},{jobs[1].id},{fake_uuid}" - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert len(data["data"]["jobs"]) == 2 @@ -203,89 +178,78 @@ def test_get_mixed_existing_nonexisting_jobs( assert data["data"]["jobs_not_found"][0] == fake_uuid def test_get_jobs_with_empty_string( - self, - client: TestClient, - user_api_key: APIKeyPublic + self, client: TestClient, user_api_key: APIKeyPublic ): """Test retrieving jobs with empty job_ids parameter.""" response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids=", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert len(data["data"]["jobs"]) == 0 assert len(data["data"]["jobs_not_found"]) == 0 def test_get_jobs_with_whitespace_only( - self, - client: TestClient, - user_api_key: APIKeyPublic + self, client: TestClient, user_api_key: APIKeyPublic ): """Test retrieving jobs with whitespace-only job_ids.""" response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids= , , ", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert len(data["data"]["jobs"]) == 0 assert len(data["data"]["jobs_not_found"]) == 0 def test_get_jobs_invalid_uuid_format_422( - self, - client: TestClient, - user_api_key: APIKeyPublic + self, client: TestClient, user_api_key: APIKeyPublic ): """Test that invalid UUID format returns 422.""" invalid_uuids = "not-a-uuid,also-not-uuid" - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids={invalid_uuids}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 422 data = response.json() assert "Invalid UUID(s) provided" in data["error"] def test_get_jobs_mixed_valid_invalid_uuid_422( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test that mixed valid/invalid UUIDs returns 422.""" store = DocumentStore(db, user_api_key.project_id) crud = DocTransformationJobCrud(db, user_api_key.project_id) document = store.put() job = crud.create(document.id) - + job_ids_str = f"{job.id},not-a-uuid" - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 422 data = response.json() assert "Invalid UUID(s) provided" in data["error"] assert "not-a-uuid" in data["error"] def test_get_jobs_missing_parameter_422( - self, - client: TestClient, - user_api_key: APIKeyPublic + self, client: TestClient, user_api_key: APIKeyPublic ): """Test that missing job_ids parameter returns 422.""" response = client.get( f"{settings.API_V1_STR}/documents/transformations/", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 422 def test_get_jobs_different_project_not_found( @@ -293,20 +257,20 @@ def test_get_jobs_different_project_not_found( client: TestClient, db: Session, user_api_key: APIKeyPublic, - superuser_api_key: APIKeyPublic + superuser_api_key: APIKeyPublic, ): """Test that jobs from different projects are not returned.""" store = DocumentStore(db, user_api_key.project_id) crud = DocTransformationJobCrud(db, user_api_key.project_id) document = store.put() job = crud.create(document.id) - + # Try to access with user from different project (superuser) response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids={job.id}", - headers={"X-API-KEY": superuser_api_key.key} + headers={"X-API-KEY": superuser_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert len(data["data"]["jobs"]) == 0 @@ -314,38 +278,41 @@ def test_get_jobs_different_project_not_found( assert data["data"]["jobs_not_found"][0] == str(job.id) def test_get_jobs_with_various_statuses( - self, - client: TestClient, - db: Session, - user_api_key: APIKeyPublic + self, client: TestClient, db: Session, user_api_key: APIKeyPublic ): """Test retrieving jobs with different statuses.""" store = DocumentStore(db, user_api_key.project_id) crud = DocTransformationJobCrud(db, user_api_key.project_id) documents = store.fill(4) jobs = [crud.create(doc.id) for doc in documents] - + crud.update_status(jobs[1].id, TransformationStatus.PROCESSING) - crud.update_status(jobs[2].id, TransformationStatus.COMPLETED, transformed_document_id=documents[2].id) - crud.update_status(jobs[3].id, TransformationStatus.FAILED, error_message="Test error") - + crud.update_status( + jobs[2].id, + TransformationStatus.COMPLETED, + transformed_document_id=documents[2].id, + ) + crud.update_status( + jobs[3].id, TransformationStatus.FAILED, error_message="Test error" + ) + job_ids_str = ",".join(str(job.id) for job in jobs) - + response = client.get( f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", - headers={"X-API-KEY": user_api_key.key} + headers={"X-API-KEY": user_api_key.key}, ) - + assert response.status_code == 200 data = response.json() assert len(data["data"]["jobs"]) == 4 - + # Check that all statuses are represented statuses = {job["status"] for job in data["data"]["jobs"]} expected_statuses = { TransformationStatus.PENDING, TransformationStatus.PROCESSING, TransformationStatus.COMPLETED, - TransformationStatus.FAILED + TransformationStatus.FAILED, } assert statuses == expected_statuses diff --git a/backend/app/tests/core/doctransformer/test_service/base.py b/backend/app/tests/core/doctransformer/test_service/base.py index f6e14904..ecdf6016 100644 --- a/backend/app/tests/core/doctransformer/test_service/base.py +++ b/backend/app/tests/core/doctransformer/test_service/base.py @@ -20,57 +20,52 @@ class DocTransformTestBase: """Base class for document transformation tests with common setup and utilities.""" - + def setup_aws_s3(self) -> AmazonCloudStorageClient: """Setup AWS S3 for testing.""" aws = AmazonCloudStorageClient() aws.create() return aws - + def create_s3_document_content( - self, - aws: AmazonCloudStorageClient, - project: Project, - document: Document, - content: bytes = b"Test document content" + self, + aws: AmazonCloudStorageClient, + project: Project, + document: Document, + content: bytes = b"Test document content", ) -> bytes: """Create content in S3 for a document.""" parsed_url = urlparse(document.object_store_url) - s3_key = parsed_url.path.lstrip('/') - - aws.client.put_object( - Bucket=settings.AWS_S3_BUCKET, - Key=s3_key, - Body=content - ) + s3_key = parsed_url.path.lstrip("/") + + aws.client.put_object(Bucket=settings.AWS_S3_BUCKET, Key=s3_key, Body=content) return content - + def verify_s3_content( - self, - aws: AmazonCloudStorageClient, - project: Project, + self, + aws: AmazonCloudStorageClient, + project: Project, transformed_doc: Document, - expected_content: str = None + expected_content: str = None, ) -> None: """Verify the content stored in S3.""" if expected_content is None: expected_content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - + parsed_url = urlparse(transformed_doc.object_store_url) - transformed_key = parsed_url.path.lstrip('/') - + transformed_key = parsed_url.path.lstrip("/") + response = aws.client.get_object( - Bucket=settings.AWS_S3_BUCKET, - Key=transformed_key + Bucket=settings.AWS_S3_BUCKET, Key=transformed_key ) - transformed_content = response['Body'].read().decode('utf-8') + transformed_content = response["Body"].read().decode("utf-8") assert transformed_content == expected_content class TestDataProvider: """Provides test data and configurations for document transformation tests.""" - + @staticmethod def get_format_test_cases() -> List[tuple]: """Get test cases for different document formats.""" @@ -79,7 +74,7 @@ def get_format_test_cases() -> List[tuple]: ("text", ".txt"), ("html", ".html"), ] - + @staticmethod def get_content_type_test_cases() -> List[tuple]: """Get test cases for content types and extensions.""" @@ -87,9 +82,9 @@ def get_content_type_test_cases() -> List[tuple]: ("markdown", "text/markdown", ".md"), ("text", "text/plain", ".txt"), ("html", "text/html", ".html"), - ("unknown", "text/plain", ".unknown") # Default fallback + ("unknown", "text/plain", ".unknown"), # Default fallback ] - + @staticmethod def get_test_transformer_names() -> List[str]: """Get list of test transformer names.""" @@ -103,26 +98,32 @@ def get_sample_document_content() -> bytes: class MockHelpers: """Helper methods for creating mocks in tests.""" - + @staticmethod def create_failing_convert_document(fail_count: int = 1): """Create a side effect function that fails specified times then succeeds.""" call_count = 0 + def failing_convert_document(*args, **kwargs): nonlocal call_count call_count += 1 if call_count <= fail_count: raise Exception("Transient error") - output_path = args[1] if len(args) > 1 else kwargs.get('output_path') + output_path = args[1] if len(args) > 1 else kwargs.get("output_path") if output_path: - output_path.write_text("Success after retries", encoding='utf-8') + output_path.write_text("Success after retries", encoding="utf-8") return output_path raise ValueError("output_path is required") + return failing_convert_document - + @staticmethod - def create_persistent_failing_convert_document(error_message: str = "Persistent error"): + def create_persistent_failing_convert_document( + error_message: str = "Persistent error", + ): """Create a side effect function that always fails.""" + def persistent_failing_convert_document(*args, **kwargs): raise Exception(error_message) + return persistent_failing_convert_document diff --git a/backend/app/tests/core/doctransformer/test_service/conftest.py b/backend/app/tests/core/doctransformer/test_service/conftest.py index d69e8d11..7f3aeda0 100644 --- a/backend/app/tests/core/doctransformer/test_service/conftest.py +++ b/backend/app/tests/core/doctransformer/test_service/conftest.py @@ -33,15 +33,21 @@ def aws_credentials() -> None: def fast_execute_job() -> Generator[Callable[[int, UUID, str, str], Any], None, None]: """Create a version of execute_job without retry delays for faster testing.""" from app.core.doctransform import service - + original_execute_job = service.execute_job - - @retry(stop=stop_after_attempt(2), wait=wait_fixed(0.01)) # Very fast retry for tests - def fast_execute_job_func(project_id: int, job_id: UUID, transformer_name: str, target_format: str) -> Any: + + @retry( + stop=stop_after_attempt(2), wait=wait_fixed(0.01) + ) # Very fast retry for tests + def fast_execute_job_func( + project_id: int, job_id: UUID, transformer_name: str, target_format: str + ) -> Any: # Call the original function's implementation without the decorator - return original_execute_job.__wrapped__(project_id, job_id, transformer_name, target_format) - - with patch.object(service, 'execute_job', fast_execute_job_func): + return original_execute_job.__wrapped__( + project_id, job_id, transformer_name, target_format + ) + + with patch.object(service, "execute_job", fast_execute_job_func): yield fast_execute_job_func @@ -64,7 +70,9 @@ def background_tasks() -> BackgroundTasks: @pytest.fixture -def test_document(db: Session, current_user: UserProjectOrg) -> Tuple[Document, Project]: +def test_document( + db: Session, current_user: UserProjectOrg +) -> Tuple[Document, Project]: """Create a test document for the current user's project.""" store = DocumentStore(db, current_user.project_id) project = get_project_by_id(session=db, project_id=current_user.project_id) diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py index b3b7e2f4..250cb0e6 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_execute_job.py +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py @@ -15,7 +15,10 @@ from app.core.doctransform.service import execute_job from app.core.exception_handlers import HTTPException from app.models import Document, DocTransformationJob, Project, TransformationStatus -from app.tests.core.doctransformer.test_service.base import DocTransformTestBase, TestDataProvider +from app.tests.core.doctransformer.test_service.base import ( + DocTransformTestBase, + TestDataProvider, +) class TestExecuteJob(DocTransformTestBase): @@ -37,7 +40,7 @@ def test_execute_job_success( """Test successful document transformation with multiple formats.""" document, project = test_document aws = self.setup_aws_s3() - + source_content = TestDataProvider.get_sample_document_content() self.create_s3_document_content(aws, project, document, source_content) @@ -45,17 +48,17 @@ def test_execute_job_success( job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - + # Mock the Session to use our existing database session - with patch('app.core.doctransform.service.Session') as mock_session_class: + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format=target_format + target_format=target_format, ) # Verify job completion @@ -79,56 +82,56 @@ def test_execute_job_success( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_with_nonexistent_job( - self, - db: Session, - test_document: Tuple[Document, Project], - fast_execute_job: Callable[[int, UUID, str, str], Any] + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, UUID, str, str], Any], ) -> None: """Test job execution with non-existent job ID.""" _, project = test_document self.setup_aws_s3() nonexistent_job_id = uuid4() - - with patch('app.core.doctransform.service.Session') as mock_session_class: + + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + # Execute job should fail because job doesn't exist with pytest.raises((HTTPException, RetryError)): fast_execute_job( project_id=project.id, job_id=nonexistent_job_id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_with_missing_source_document( - self, - db: Session, - test_document: Tuple[Document, Project], - fast_execute_job: Callable[[int, UUID, str, str], Any] + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, UUID, str, str], Any], ) -> None: """Test job execution when source document is missing from S3.""" document, project = test_document self.setup_aws_s3() - + # Create job but don't upload document to S3 job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - - with patch('app.core.doctransform.service.Session') as mock_session_class: + + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + with pytest.raises(Exception): fast_execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify job was marked as failed @@ -140,10 +143,10 @@ def test_execute_job_with_missing_source_document( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_with_transformer_error( - self, - db: Session, - test_document: Tuple[Document, Project], - fast_execute_job: Callable[[int, UUID, str, str], Any] + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, UUID, str, str], Any], ) -> None: """Test job execution when transformer raises an error.""" document, project = test_document @@ -153,22 +156,24 @@ def test_execute_job_with_transformer_error( job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - + # Mock convert_document to raise TransformationError - with patch('app.core.doctransform.service.Session') as mock_session_class, \ - patch('app.core.doctransform.service.convert_document') as mock_convert: - + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.service.convert_document" + ) as mock_convert: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None mock_convert.side_effect = TransformationError("Mock transformation error") - + # Due to retry mechanism, it will raise RetryError after exhausting retries with pytest.raises((TransformationError, RetryError)): fast_execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify job was marked as failed @@ -180,9 +185,7 @@ def test_execute_job_with_transformer_error( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_status_transitions( - self, - db: Session, - test_document: Tuple[Document, Project] + self, db: Session, test_document: Tuple[Document, Project] ) -> None: """Test that job status transitions correctly during execution.""" document, project = test_document @@ -193,16 +196,16 @@ def test_execute_job_status_transitions( job = job_crud.create(source_document_id=document.id) initial_status = job.status db.commit() - - with patch('app.core.doctransform.service.Session') as mock_session_class: + + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify status progression by checking final job state @@ -210,12 +213,10 @@ def test_execute_job_status_transitions( assert job.status == TransformationStatus.COMPLETED assert initial_status == TransformationStatus.PENDING - @mock_aws + @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_with_different_content_types( - self, - db: Session, - test_document: Tuple[Document, Project] + self, db: Session, test_document: Tuple[Document, Project] ) -> None: """Test job execution produces correct content types for different formats.""" document, project = test_document @@ -223,23 +224,27 @@ def test_execute_job_with_different_content_types( self.create_s3_document_content(aws, project, document) format_extensions = TestDataProvider.get_content_type_test_cases() - - for target_format, expected_content_type, expected_extension in format_extensions: + + for ( + target_format, + expected_content_type, + expected_extension, + ) in format_extensions: job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - - with patch('app.core.doctransform.service.Session') as mock_session_class: + + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format=target_format + target_format=target_format, ) - + # Verify transformation completed and check file extension db.refresh(job) assert job.status == TransformationStatus.COMPLETED diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py index 80343865..9b3b9807 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py @@ -13,7 +13,10 @@ from app.crud import DocTransformationJobCrud from app.core.doctransform.service import execute_job from app.models import Document, Project, TransformationStatus -from app.tests.core.doctransformer.test_service.base import DocTransformTestBase, MockHelpers +from app.tests.core.doctransformer.test_service.base import ( + DocTransformTestBase, + MockHelpers, +) class TestExecuteJobRetryAndErrors(DocTransformTestBase): @@ -22,10 +25,10 @@ class TestExecuteJobRetryAndErrors(DocTransformTestBase): @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_with_storage_error( - self, - db: Session, - test_document: Tuple[Document, Project], - fast_execute_job: Callable[[int, Any, str, str], Any] + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any], ) -> None: """Test job execution when S3 upload fails.""" document, project = test_document @@ -35,24 +38,26 @@ def test_execute_job_with_storage_error( job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - - # Mock storage.put to raise an error - with patch('app.core.doctransform.service.Session') as mock_session_class, \ - patch('app.core.doctransform.service.get_cloud_storage') as mock_storage_class: + # Mock storage.put to raise an error + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.service.get_cloud_storage" + ) as mock_storage_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + mock_storage = mock_storage_class.return_value mock_storage.stream.return_value = BytesIO(b"test content") mock_storage.put.side_effect = Exception("S3 upload failed") - + with pytest.raises(Exception): fast_execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify job was marked as failed @@ -64,10 +69,10 @@ def test_execute_job_with_storage_error( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_retry_mechanism( - self, - db: Session, - test_document: Tuple[Document, Project], - fast_execute_job: Callable[[int, Any, str, str], Any] + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any], ) -> None: """Test that retry mechanism works for transient failures.""" document, project = test_document @@ -77,21 +82,26 @@ def test_execute_job_retry_mechanism( job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - + # Create a side effect that fails once then succeeds (fast retry will only try 2 times) - failing_convert_document = MockHelpers.create_failing_convert_document(fail_count=1) - - with patch('app.core.doctransform.service.Session') as mock_session_class, \ - patch('app.core.doctransform.service.convert_document', side_effect=failing_convert_document): - + failing_convert_document = MockHelpers.create_failing_convert_document( + fail_count=1 + ) + + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.service.convert_document", + side_effect=failing_convert_document, + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + fast_execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify the function was retried and eventually succeeded @@ -101,10 +111,10 @@ def test_execute_job_retry_mechanism( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_exhausted_retries( - self, - db: Session, - test_document: Tuple[Document, Project], - fast_execute_job: Callable[[int, Any, str, str], Any] + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any], ) -> None: """Test behavior when all retry attempts are exhausted.""" document, project = test_document @@ -114,22 +124,27 @@ def test_execute_job_exhausted_retries( job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - + # Mock convert_document to always fail - persistent_failing_convert_document = MockHelpers.create_persistent_failing_convert_document("Persistent error") - - with patch('app.core.doctransform.service.Session') as mock_session_class, \ - patch('app.core.doctransform.service.convert_document', side_effect=persistent_failing_convert_document): - + persistent_failing_convert_document = ( + MockHelpers.create_persistent_failing_convert_document("Persistent error") + ) + + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.service.convert_document", + side_effect=persistent_failing_convert_document, + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + with pytest.raises(Exception): fast_execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify job was marked as failed after retries @@ -140,10 +155,10 @@ def test_execute_job_exhausted_retries( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_database_error_during_completion( - self, - db: Session, - test_document: Tuple[Document, Project], - fast_execute_job: Callable[[int, Any, str, str], Any] + self, + db: Session, + test_document: Tuple[Document, Project], + fast_execute_job: Callable[[int, Any, str, str], Any], ) -> None: """Test handling of database errors when updating job completion.""" document, project = test_document @@ -153,23 +168,29 @@ def test_execute_job_database_error_during_completion( job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() - - with patch('app.core.doctransform.service.Session') as mock_session_class: + + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + # Mock DocumentCrud.update to fail when creating the transformed document - with patch('app.core.doctransform.service.DocumentCrud') as mock_doc_crud_class: + with patch( + "app.core.doctransform.service.DocumentCrud" + ) as mock_doc_crud_class: mock_doc_crud_instance = mock_doc_crud_class.return_value - mock_doc_crud_instance.read_one.return_value = document # Return valid document for source - mock_doc_crud_instance.update.side_effect = Exception("Database error during document creation") - + mock_doc_crud_instance.read_one.return_value = ( + document # Return valid document for source + ) + mock_doc_crud_instance.update.side_effect = Exception( + "Database error during document creation" + ) + with pytest.raises(Exception): fast_execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify job was marked as failed diff --git a/backend/app/tests/core/doctransformer/test_service/test_integration.py b/backend/app/tests/core/doctransformer/test_service/test_integration.py index 4c912b32..76f015e5 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_integration.py +++ b/backend/app/tests/core/doctransformer/test_service/test_integration.py @@ -11,7 +11,13 @@ from app.crud import DocTransformationJobCrud, DocumentCrud from app.core.doctransform.service import execute_job, start_job -from app.models import Document, DocTransformationJob, Project, TransformationStatus, UserProjectOrg +from app.models import ( + Document, + DocTransformationJob, + Project, + TransformationStatus, + UserProjectOrg, +) from app.tests.core.doctransformer.test_service.base import DocTransformTestBase @@ -21,9 +27,7 @@ class TestExecuteJobIntegration(DocTransformTestBase): @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_end_to_end_workflow( - self, - db: Session, - test_document: Tuple[Document, Project] + self, db: Session, test_document: Tuple[Document, Project] ) -> None: """Test complete end-to-end workflow from start_job to execute_job.""" document, project = test_document @@ -35,7 +39,7 @@ def test_execute_job_end_to_end_workflow( id=1, email="test@example.com", project_id=project.id, - organization_id=project.organization_id + organization_id=project.organization_id, ) background_tasks = BackgroundTasks() @@ -53,22 +57,22 @@ def test_execute_job_end_to_end_workflow( assert job.status == TransformationStatus.PENDING # Execute the job manually (simulating background execution) - with patch('app.core.doctransform.service.Session') as mock_session_class: + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify complete workflow db.refresh(job) assert job.status == TransformationStatus.COMPLETED assert job.transformed_document_id is not None - + # Verify transformed document exists and is valid document_crud = DocumentCrud(session=db, project_id=project.id) transformed_doc = document_crud.read_one(job.transformed_document_id) @@ -78,9 +82,7 @@ def test_execute_job_end_to_end_workflow( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_execute_job_concurrent_jobs( - self, - db: Session, - test_document: Tuple[Document, Project] + self, db: Session, test_document: Tuple[Document, Project] ) -> None: """Test multiple concurrent job executions don't interfere with each other.""" document, project = test_document @@ -94,18 +96,18 @@ def test_execute_job_concurrent_jobs( job = job_crud.create(source_document_id=document.id) jobs.append(job) db.commit() - + # Execute all jobs for job in jobs: - with patch('app.core.doctransform.service.Session') as mock_session_class: + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format="markdown" + target_format="markdown", ) # Verify all jobs completed successfully @@ -117,9 +119,7 @@ def test_execute_job_concurrent_jobs( @mock_aws @pytest.mark.usefixtures("aws_credentials") def test_multiple_format_transformations( - self, - db: Session, - test_document: Tuple[Document, Project] + self, db: Session, test_document: Tuple[Document, Project] ) -> None: """Test transforming the same document to multiple formats.""" document, project = test_document @@ -128,25 +128,25 @@ def test_multiple_format_transformations( formats = ["markdown", "text", "html"] jobs = [] - + # Create jobs for different formats job_crud = DocTransformationJobCrud(session=db, project_id=project.id) for target_format in formats: job = job_crud.create(source_document_id=document.id) jobs.append((job, target_format)) db.commit() - + # Execute all jobs for job, target_format in jobs: - with patch('app.core.doctransform.service.Session') as mock_session_class: + with patch("app.core.doctransform.service.Session") as mock_session_class: mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None - + execute_job( project_id=project.id, job_id=job.id, transformer_name="test", - target_format=target_format + target_format=target_format, ) # Verify all jobs completed successfully with correct formats @@ -155,7 +155,7 @@ def test_multiple_format_transformations( db.refresh(job) assert job.status == TransformationStatus.COMPLETED assert job.transformed_document_id is not None - + transformed_doc = document_crud.read_one(job.transformed_document_id) assert transformed_doc is not None # Verify correct file extension based on format diff --git a/backend/app/tests/core/doctransformer/test_service/test_start_job.py b/backend/app/tests/core/doctransformer/test_service/test_start_job.py index 62274825..18bd5c28 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_start_job.py +++ b/backend/app/tests/core/doctransformer/test_service/test_start_job.py @@ -10,19 +10,28 @@ from app.core.doctransform.service import execute_job, start_job from app.core.exception_handlers import HTTPException -from app.models import Document, DocTransformationJob, Project, TransformationStatus, UserProjectOrg -from app.tests.core.doctransformer.test_service.base import DocTransformTestBase, TestDataProvider +from app.models import ( + Document, + DocTransformationJob, + Project, + TransformationStatus, + UserProjectOrg, +) +from app.tests.core.doctransformer.test_service.base import ( + DocTransformTestBase, + TestDataProvider, +) class TestStartJob(DocTransformTestBase): """Test cases for the start_job function.""" - + def test_start_job_success( - self, - db: Session, - current_user: UserProjectOrg, - test_document: Tuple[Document, Project], - background_tasks: BackgroundTasks + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], + background_tasks: BackgroundTasks, ) -> None: """Test successful job creation and scheduling.""" document, _ = test_document @@ -51,14 +60,14 @@ def test_start_job_success( assert task.args[3] == "markdown" def test_start_job_with_nonexistent_document( - self, - db: Session, - current_user: UserProjectOrg, - background_tasks: BackgroundTasks + self, + db: Session, + current_user: UserProjectOrg, + background_tasks: BackgroundTasks, ) -> None: """Test job creation with non-existent document raises error.""" nonexistent_id = uuid4() - + with pytest.raises(HTTPException) as exc_info: start_job( db=db, @@ -68,16 +77,16 @@ def test_start_job_with_nonexistent_document( target_format="markdown", background_tasks=background_tasks, ) - + assert exc_info.value.status_code == 404 assert "Document not found" in str(exc_info.value.detail) def test_start_job_with_deleted_document( - self, - db: Session, - current_user: UserProjectOrg, - test_document: Tuple[Document, Project], - background_tasks: BackgroundTasks + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], + background_tasks: BackgroundTasks, ) -> None: """Test job creation with deleted document raises error.""" document, _ = test_document @@ -85,7 +94,7 @@ def test_start_job_with_deleted_document( document.is_deleted = True db.add(document) db.commit() - + with pytest.raises(HTTPException) as exc_info: start_job( db=db, @@ -95,21 +104,21 @@ def test_start_job_with_deleted_document( target_format="markdown", background_tasks=background_tasks, ) - + assert exc_info.value.status_code == 404 assert "Document not found" in str(exc_info.value.detail) def test_start_job_with_different_formats( - self, - db: Session, - current_user: UserProjectOrg, - test_document: Tuple[Document, Project], - background_tasks: BackgroundTasks + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], + background_tasks: BackgroundTasks, ) -> None: """Test job creation with different target formats.""" document, _ = test_document formats = ["markdown", "text", "html"] - + for target_format in formats: job_id = start_job( db=db, @@ -119,26 +128,28 @@ def test_start_job_with_different_formats( target_format=target_format, background_tasks=background_tasks, ) - + job = db.get(DocTransformationJob, job_id) assert job is not None assert job.status == TransformationStatus.PENDING - + task = background_tasks.tasks[-1] assert task.args[3] == target_format - @pytest.mark.parametrize("transformer_name", TestDataProvider.get_test_transformer_names()) + @pytest.mark.parametrize( + "transformer_name", TestDataProvider.get_test_transformer_names() + ) def test_start_job_with_different_transformers( - self, - db: Session, - current_user: UserProjectOrg, - test_document: Tuple[Document, Project], + self, + db: Session, + current_user: UserProjectOrg, + test_document: Tuple[Document, Project], background_tasks: BackgroundTasks, - transformer_name: str + transformer_name: str, ) -> None: """Test job creation with different transformer names.""" document, _ = test_document - + job_id = start_job( db=db, current_user=current_user, @@ -147,10 +158,10 @@ def test_start_job_with_different_transformers( target_format="markdown", background_tasks=background_tasks, ) - + job = db.get(DocTransformationJob, job_id) assert job is not None assert job.status == TransformationStatus.PENDING - + task = background_tasks.tasks[-1] assert task.args[2] == transformer_name diff --git a/backend/app/tests/crud/test_doc_transformation_job.py b/backend/app/tests/crud/test_doc_transformation_job.py index 01edfbee..4f5f3ce6 100644 --- a/backend/app/tests/crud/test_doc_transformation_job.py +++ b/backend/app/tests/crud/test_doc_transformation_job.py @@ -20,12 +20,14 @@ def crud(db: Session, store: DocumentStore): class TestDocTransformationJobCrudCreate: - def test_can_create_job_with_valid_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_can_create_job_with_valid_document( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test creating a transformation job with a valid source document.""" document = store.put() - + job = crud.create(document.id) - + assert job.id is not None assert job.source_document_id == document.id assert job.status == TransformationStatus.PENDING @@ -34,197 +36,227 @@ def test_can_create_job_with_valid_document(self, db: Session, store: DocumentSt assert job.created_at is not None assert job.updated_at is not None - def test_cannot_create_job_with_invalid_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_cannot_create_job_with_invalid_document( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test that creating a job with non-existent document raises an error.""" invalid_id = next(SequentialUuidGenerator()) - + with pytest.raises(HTTPException) as exc_info: crud.create(invalid_id) - + assert exc_info.value.status_code == 404 assert "Document not found" in str(exc_info.value.detail) - def test_cannot_create_job_with_deleted_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_cannot_create_job_with_deleted_document( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test that creating a job with a deleted document raises an error.""" document = store.put() # Mark document as deleted document.is_deleted = True db.add(document) db.commit() - + with pytest.raises(HTTPException) as exc_info: crud.create(document.id) - + assert exc_info.value.status_code == 404 assert "Document not found" in str(exc_info.value.detail) class TestDocTransformationJobCrudReadOne: - def test_can_read_existing_job(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_can_read_existing_job( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test reading an existing transformation job.""" document = store.put() job = crud.create(document.id) - + result = crud.read_one(job.id) - + assert result.id == job.id assert result.source_document_id == document.id assert result.status == TransformationStatus.PENDING - def test_cannot_read_nonexistent_job(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_cannot_read_nonexistent_job( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test that reading a non-existent job raises an error.""" invalid_id = next(SequentialUuidGenerator()) - + with pytest.raises(HTTPException) as exc_info: crud.read_one(invalid_id) - + assert exc_info.value.status_code == 404 assert "Transformation job not found" in str(exc_info.value.detail) - def test_cannot_read_job_with_deleted_document(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_cannot_read_job_with_deleted_document( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test that reading a job whose source document is deleted raises an error.""" document = store.put() job = crud.create(document.id) - + # Mark document as deleted document.is_deleted = True db.add(document) db.commit() - + with pytest.raises(HTTPException) as exc_info: crud.read_one(job.id) - + assert exc_info.value.status_code == 404 assert "Transformation job not found" in str(exc_info.value.detail) - def test_cannot_read_job_from_different_project(self, db: Session, store: DocumentStore): + def test_cannot_read_job_from_different_project( + self, db: Session, store: DocumentStore + ): """Test that reading a job from a different project raises an error.""" document = store.put() job_crud = DocTransformationJobCrud(db, store.project.id) job = job_crud.create(document.id) - + # Try to read from different project other_project = create_test_project(db) other_crud = DocTransformationJobCrud(db, other_project.id) - + with pytest.raises(HTTPException) as exc_info: other_crud.read_one(job.id) - + assert exc_info.value.status_code == 404 assert "Transformation job not found" in str(exc_info.value.detail) class TestDocTransformationJobCrudReadEach: - def test_can_read_multiple_existing_jobs(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_can_read_multiple_existing_jobs( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test reading multiple existing transformation jobs.""" documents = store.fill(3) jobs = [crud.create(doc.id) for doc in documents] job_ids = {job.id for job in jobs} - + results = crud.read_each(job_ids) - + assert len(results) == 3 result_ids = {job.id for job in results} assert result_ids == job_ids - def test_read_partial_existing_jobs(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_read_partial_existing_jobs( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test reading a mix of existing and non-existing jobs.""" documents = store.fill(2) jobs = [crud.create(doc.id) for doc in documents] job_ids = {job.id for job in jobs} job_ids.add(next(SequentialUuidGenerator())) # Add non-existent ID - + results = crud.read_each(job_ids) - + assert len(results) == 2 # Only existing jobs returned result_ids = {job.id for job in results} assert result_ids == {job.id for job in jobs} - def test_read_empty_job_set(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_read_empty_job_set( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test reading an empty set of job IDs.""" results = crud.read_each(set()) - + assert len(results) == 0 - def test_cannot_read_jobs_from_different_project(self, db: Session, store: DocumentStore): + def test_cannot_read_jobs_from_different_project( + self, db: Session, store: DocumentStore + ): """Test that jobs from different projects are not returned.""" document = store.put() job_crud = DocTransformationJobCrud(db, store.project.id) job = job_crud.create(document.id) - + # Try to read from different project other_project = get_project(db, name="Dalgo") other_crud = DocTransformationJobCrud(db, other_project.id) - + results = other_crud.read_each({job.id}) - + assert len(results) == 0 class TestDocTransformationJobCrudUpdateStatus: - def test_can_update_status_to_processing(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_can_update_status_to_processing( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test updating job status to processing.""" document = store.put() job = crud.create(document.id) - + updated_job = crud.update_status(job.id, TransformationStatus.PROCESSING) - + assert updated_job.id == job.id assert updated_job.status == TransformationStatus.PROCESSING assert updated_job.updated_at >= job.updated_at - def test_can_update_status_to_completed_with_result(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_can_update_status_to_completed_with_result( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test updating job status to completed with transformed document.""" source_document = store.put() transformed_document = store.put() job = crud.create(source_document.id) - + updated_job = crud.update_status( job.id, TransformationStatus.COMPLETED, - transformed_document_id=transformed_document.id + transformed_document_id=transformed_document.id, ) - + assert updated_job.status == TransformationStatus.COMPLETED assert updated_job.transformed_document_id == transformed_document.id assert updated_job.error_message is None - def test_can_update_status_to_failed_with_error(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_can_update_status_to_failed_with_error( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test updating job status to failed with error message.""" document = store.put() job = crud.create(document.id) error_msg = "Transformation failed due to invalid format" - + updated_job = crud.update_status( - job.id, - TransformationStatus.FAILED, - error_message=error_msg + job.id, TransformationStatus.FAILED, error_message=error_msg ) - + assert updated_job.status == TransformationStatus.FAILED assert updated_job.error_message == error_msg assert updated_job.transformed_document_id is None - def test_cannot_update_nonexistent_job(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_cannot_update_nonexistent_job( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test that updating a non-existent job raises an error.""" invalid_id = next(SequentialUuidGenerator()) - + with pytest.raises(HTTPException) as exc_info: crud.update_status(invalid_id, TransformationStatus.PROCESSING) - + assert exc_info.value.status_code == 404 assert "Transformation job not found" in str(exc_info.value.detail) - def test_update_preserves_existing_fields(self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud): + def test_update_preserves_existing_fields( + self, db: Session, store: DocumentStore, crud: DocTransformationJobCrud + ): """Test that updating status preserves other fields when not specified.""" document = store.put() job = crud.create(document.id) - + # First update with error message - crud.update_status(job.id, TransformationStatus.FAILED, error_message="Initial error") - + crud.update_status( + job.id, TransformationStatus.FAILED, error_message="Initial error" + ) + # Second update without error message - should preserve it updated_job = crud.update_status(job.id, TransformationStatus.PROCESSING) - + assert updated_job.status == TransformationStatus.PROCESSING assert updated_job.error_message == "Initial error" # Should be preserved diff --git a/backend/app/tests/utils/document.py b/backend/app/tests/utils/document.py index 674035fc..5db59658 100644 --- a/backend/app/tests/utils/document.py +++ b/backend/app/tests/utils/document.py @@ -27,14 +27,15 @@ class DocumentMaker: def __init__(self, project_id: int, session: Session): self.project_id = project_id self.session = session - self.project: Project = get_project_by_id(session=self.session, project_id=self.project_id) + self.project: Project = get_project_by_id( + session=self.session, project_id=self.project_id + ) self.index = SequentialUuidGenerator() def __iter__(self): return self def __next__(self): - doc_id = next(self.index) key = f"{self.project.storage_path}/{doc_id}.txt" object_store_url = f"s3://{settings.AWS_S3_BUCKET}/{key}" From af8ab26ddd3362af2c1c4c424430e98be4105f92 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:47:14 +0530 Subject: [PATCH 04/13] avoid relative import --- backend/app/core/doctransform/registry.py | 6 +++--- backend/app/core/doctransform/test_transformer.py | 2 +- backend/app/core/doctransform/zerox_transformer.py | 10 ++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/app/core/doctransform/registry.py b/backend/app/core/doctransform/registry.py index e75f2247..39cfedd6 100644 --- a/backend/app/core/doctransform/registry.py +++ b/backend/app/core/doctransform/registry.py @@ -1,9 +1,9 @@ from pathlib import Path from typing import Type, Dict, Set, Tuple, Optional -from .transformer import Transformer -from .test_transformer import TestTransformer -from .zerox_transformer import ZeroxTransformer +from app.core.doctransform.transformer import Transformer +from app.core.doctransform.test_transformer import TestTransformer +from app.core.doctransform.zerox_transformer import ZeroxTransformer class TransformationError(Exception): diff --git a/backend/app/core/doctransform/test_transformer.py b/backend/app/core/doctransform/test_transformer.py index 6f3b274b..28370525 100644 --- a/backend/app/core/doctransform/test_transformer.py +++ b/backend/app/core/doctransform/test_transformer.py @@ -1,5 +1,5 @@ from pathlib import Path -from .transformer import Transformer +from app.core.doctransform.transformer import Transformer class TestTransformer(Transformer): diff --git a/backend/app/core/doctransform/zerox_transformer.py b/backend/app/core/doctransform/zerox_transformer.py index 2c65253f..141e7164 100644 --- a/backend/app/core/doctransform/zerox_transformer.py +++ b/backend/app/core/doctransform/zerox_transformer.py @@ -1,9 +1,11 @@ from asyncio import Runner import logging from pathlib import Path -from .transformer import Transformer +from app.core.doctransform.transformer import Transformer from pyzerox import zerox +logger = logging.getLogger(__name__) + class ZeroxTransformer(Transformer): """ @@ -14,7 +16,7 @@ def __init__(self, model: str = "gpt-4o"): self.model = model def transform(self, input_path: Path, output_path: Path) -> Path: - logging.info(f"ZeroxTransformer Started: (model={self.model})") + logger.info(f"ZeroxTransformer Started: (model={self.model})") if not input_path.exists(): raise FileNotFoundError(f"Input file not found: {input_path}") @@ -38,12 +40,12 @@ def transform(self, input_path: Path, output_path: Path) -> Path: output_file.write(page.content) output_file.write("\n\n") - logging.info( + logger.info( f"[ZeroxTransformer.transform] Transformation completed, output written to: {output_path}" ) return output_path except Exception as e: - logging.error( + logger.error( f"ZeroxTransformer failed for {input_path}: {e}\n" "This may be due to a missing Poppler installation or a corrupt PDF file.", exc_info=True, From 39166bb8774295b85deb2fd9c1a1471a7f281255 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:14:51 +0530 Subject: [PATCH 05/13] suppress logs of litellm --- backend/app/core/logger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/app/core/logger.py b/backend/app/core/logger.py index 00ceb822..0488a9db 100644 --- a/backend/app/core/logger.py +++ b/backend/app/core/logger.py @@ -20,6 +20,8 @@ def filter(self, record: logging.LogRecord) -> bool: record.correlation_id = correlation_id.get() or "N/A" return True +# Suppress info logs from LiteLLM +logging.getLogger("LiteLLM").setLevel(logging.WARNING) # Create root logger logger = logging.getLogger() From 194ce2dc82516d46640274647b1d78ab977c37f8 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:58:35 +0530 Subject: [PATCH 06/13] unit test for registry --- backend/app/core/logger.py | 1 + .../core/doctransformer/test_registry.py | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 backend/app/tests/core/doctransformer/test_registry.py diff --git a/backend/app/core/logger.py b/backend/app/core/logger.py index 0488a9db..ff147035 100644 --- a/backend/app/core/logger.py +++ b/backend/app/core/logger.py @@ -20,6 +20,7 @@ def filter(self, record: logging.LogRecord) -> bool: record.correlation_id = correlation_id.get() or "N/A" return True + # Suppress info logs from LiteLLM logging.getLogger("LiteLLM").setLevel(logging.WARNING) diff --git a/backend/app/tests/core/doctransformer/test_registry.py b/backend/app/tests/core/doctransformer/test_registry.py new file mode 100644 index 00000000..c8c2e0b2 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_registry.py @@ -0,0 +1,94 @@ +import pytest +from app.core.doctransform.registry import ( + get_file_format, + get_supported_transformations, + is_transformation_supported, + get_available_transformers, + resolve_transformer, + convert_document, + TransformationError, + TRANSFORMERS, +) + + +# Fixture for patching supported transformations +@pytest.fixture +def patched_transformations(monkeypatch): + mapping = { + ("docx", "pdf"): {"default": "pandoc", "pandoc": "pandoc"}, + ("pdf", "markdown"): {"default": "zerox", "zerox": "zerox"}, + } + monkeypatch.setattr("app.core.doctransform.registry.SUPPORTED_TRANSFORMATIONS", mapping) + return mapping + + +def test_get_file_format_valid(): + assert get_file_format("file.pdf") == "pdf" + assert get_file_format("file.docx") == "docx" + assert get_file_format("file.md") == "markdown" + assert get_file_format("file.html") == "html" + + +def test_get_file_format_invalid(): + with pytest.raises(ValueError): + get_file_format("file.unknown") + + +def test_get_supported_transformations(patched_transformations): + supported = get_supported_transformations() + assert ("docx", "pdf") in supported + assert "default" in supported[("docx", "pdf")] + assert ("pdf", "markdown") in supported + assert "zerox" in supported[("pdf", "markdown")] + + +def test_is_transformation_supported(monkeypatch): + monkeypatch.setattr( + "app.core.doctransform.registry.SUPPORTED_TRANSFORMATIONS", + {("docx", "pdf"): {"default": "pandoc"}}, + ) + assert is_transformation_supported("docx", "pdf") + assert not is_transformation_supported("pdf", "docx") + + +def test_get_available_transformers(patched_transformations): + transformers = get_available_transformers("docx", "pdf") + assert "default" in transformers + assert "pandoc" in transformers + assert get_available_transformers("pdf", "docx") == {} + + +def test_resolve_transformer(patched_transformations): + assert resolve_transformer("docx", "pdf") == "pandoc" + assert resolve_transformer("docx", "pdf", "pandoc") == "pandoc" + with pytest.raises(ValueError): + resolve_transformer("docx", "pdf", "notfound") + with pytest.raises(ValueError): + resolve_transformer("pdf", "docx") + + +def test_convert_document(tmp_path, monkeypatch): + class DummyTransformer: + def transform(self, input_path, output_path): + output_path.write_text("transformed") + return output_path + + monkeypatch.setitem(TRANSFORMERS, "dummy", DummyTransformer) + input_file = tmp_path / "input.txt" + output_file = tmp_path / "output.txt" + input_file.write_text("test") + result = convert_document(input_file, output_file, transformer_name="dummy") + assert result.read_text() == "transformed" + + # Transformer not found + with pytest.raises(ValueError): + convert_document(input_file, output_file, transformer_name="notfound") + + # Transformer raises error + class FailingTransformer: + def transform(self, input_path, output_path): + raise Exception("fail") + + monkeypatch.setitem(TRANSFORMERS, "fail", FailingTransformer) + with pytest.raises(TransformationError): + convert_document(input_file, output_file, transformer_name="fail") From b341421d2c2baa332f902ef9b23448249f4027b1 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:17:38 +0530 Subject: [PATCH 07/13] refactor unit test for service --- .../core/doctransformer/test_registry.py | 4 +- .../core/doctransformer/test_service/base.py | 129 ------------------ .../test_service/test_execute_job.py | 34 +++-- .../test_service/test_execute_job_errors.py | 23 ++-- .../test_service/test_integration.py | 8 +- .../test_service/test_start_job.py | 11 +- .../core/doctransformer/test_service/utils.py | 82 +++++++++++ 7 files changed, 122 insertions(+), 169 deletions(-) delete mode 100644 backend/app/tests/core/doctransformer/test_service/base.py create mode 100644 backend/app/tests/core/doctransformer/test_service/utils.py diff --git a/backend/app/tests/core/doctransformer/test_registry.py b/backend/app/tests/core/doctransformer/test_registry.py index c8c2e0b2..7f8303ff 100644 --- a/backend/app/tests/core/doctransformer/test_registry.py +++ b/backend/app/tests/core/doctransformer/test_registry.py @@ -18,7 +18,9 @@ def patched_transformations(monkeypatch): ("docx", "pdf"): {"default": "pandoc", "pandoc": "pandoc"}, ("pdf", "markdown"): {"default": "zerox", "zerox": "zerox"}, } - monkeypatch.setattr("app.core.doctransform.registry.SUPPORTED_TRANSFORMATIONS", mapping) + monkeypatch.setattr( + "app.core.doctransform.registry.SUPPORTED_TRANSFORMATIONS", mapping + ) return mapping diff --git a/backend/app/tests/core/doctransformer/test_service/base.py b/backend/app/tests/core/doctransformer/test_service/base.py deleted file mode 100644 index ecdf6016..00000000 --- a/backend/app/tests/core/doctransformer/test_service/base.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Test organization and base classes for DocTransform service tests. - -This module contains: -- DocTransformTestBase: Base test class with common setup -- TestDataProvider: Common test data and configurations -- MockHelpers: Utilities for creating mocks and test fixtures - -All fixtures are automatically available from conftest.py in the same directory. -Test files can import these base classes and use fixtures without additional imports. -""" -from pathlib import Path -from typing import List -from urllib.parse import urlparse - -from app.core.cloud import AmazonCloudStorageClient -from app.core.config import settings -from app.models import Document, Project - - -class DocTransformTestBase: - """Base class for document transformation tests with common setup and utilities.""" - - def setup_aws_s3(self) -> AmazonCloudStorageClient: - """Setup AWS S3 for testing.""" - aws = AmazonCloudStorageClient() - aws.create() - return aws - - def create_s3_document_content( - self, - aws: AmazonCloudStorageClient, - project: Project, - document: Document, - content: bytes = b"Test document content", - ) -> bytes: - """Create content in S3 for a document.""" - parsed_url = urlparse(document.object_store_url) - s3_key = parsed_url.path.lstrip("/") - - aws.client.put_object(Bucket=settings.AWS_S3_BUCKET, Key=s3_key, Body=content) - return content - - def verify_s3_content( - self, - aws: AmazonCloudStorageClient, - project: Project, - transformed_doc: Document, - expected_content: str = None, - ) -> None: - """Verify the content stored in S3.""" - if expected_content is None: - expected_content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - - parsed_url = urlparse(transformed_doc.object_store_url) - - transformed_key = parsed_url.path.lstrip("/") - - response = aws.client.get_object( - Bucket=settings.AWS_S3_BUCKET, Key=transformed_key - ) - transformed_content = response["Body"].read().decode("utf-8") - assert transformed_content == expected_content - - -class TestDataProvider: - """Provides test data and configurations for document transformation tests.""" - - @staticmethod - def get_format_test_cases() -> List[tuple]: - """Get test cases for different document formats.""" - return [ - ("markdown", ".md"), - ("text", ".txt"), - ("html", ".html"), - ] - - @staticmethod - def get_content_type_test_cases() -> List[tuple]: - """Get test cases for content types and extensions.""" - return [ - ("markdown", "text/markdown", ".md"), - ("text", "text/plain", ".txt"), - ("html", "text/html", ".html"), - ("unknown", "text/plain", ".unknown"), # Default fallback - ] - - @staticmethod - def get_test_transformer_names() -> List[str]: - """Get list of test transformer names.""" - return ["test"] - - @staticmethod - def get_sample_document_content() -> bytes: - """Get sample document content for testing.""" - return b"This is a test document for transformation." - - -class MockHelpers: - """Helper methods for creating mocks in tests.""" - - @staticmethod - def create_failing_convert_document(fail_count: int = 1): - """Create a side effect function that fails specified times then succeeds.""" - call_count = 0 - - def failing_convert_document(*args, **kwargs): - nonlocal call_count - call_count += 1 - if call_count <= fail_count: - raise Exception("Transient error") - output_path = args[1] if len(args) > 1 else kwargs.get("output_path") - if output_path: - output_path.write_text("Success after retries", encoding="utf-8") - return output_path - raise ValueError("output_path is required") - - return failing_convert_document - - @staticmethod - def create_persistent_failing_convert_document( - error_message: str = "Persistent error", - ): - """Create a side effect function that always fails.""" - - def persistent_failing_convert_document(*args, **kwargs): - raise Exception(error_message) - - return persistent_failing_convert_document diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py index 250cb0e6..14aded18 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_execute_job.py +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py @@ -14,11 +14,8 @@ from app.core.doctransform.registry import TransformationError from app.core.doctransform.service import execute_job from app.core.exception_handlers import HTTPException -from app.models import Document, DocTransformationJob, Project, TransformationStatus -from app.tests.core.doctransformer.test_service.base import ( - DocTransformTestBase, - TestDataProvider, -) +from app.models import Document, Project, TransformationStatus +from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase class TestExecuteJob(DocTransformTestBase): @@ -26,7 +23,11 @@ class TestExecuteJob(DocTransformTestBase): @pytest.mark.parametrize( "target_format, expected_extension", - TestDataProvider.get_format_test_cases(), + [ + ("markdown", ".md"), + ("text", ".txt"), + ("html", ".html"), + ], ) @mock_aws @pytest.mark.usefixtures("aws_credentials") @@ -41,8 +42,8 @@ def test_execute_job_success( document, project = test_document aws = self.setup_aws_s3() - source_content = TestDataProvider.get_sample_document_content() - self.create_s3_document_content(aws, project, document, source_content) + source_content = b"This is a test document for transformation." + self.create_s3_document_content(aws, document, source_content) # Create transformation job job_crud = DocTransformationJobCrud(session=db, project_id=project.id) @@ -77,7 +78,7 @@ def test_execute_job_success( assert transformed_doc.object_store_url is not None # Verify transformed content in S3 - self.verify_s3_content(aws, project, transformed_doc) + self.verify_s3_content(aws, transformed_doc) @mock_aws @pytest.mark.usefixtures("aws_credentials") @@ -151,7 +152,7 @@ def test_execute_job_with_transformer_error( """Test job execution when transformer raises an error.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) @@ -190,7 +191,7 @@ def test_execute_job_status_transitions( """Test that job status transitions correctly during execution.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) @@ -221,9 +222,14 @@ def test_execute_job_with_different_content_types( """Test job execution produces correct content types for different formats.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) - - format_extensions = TestDataProvider.get_content_type_test_cases() + self.create_s3_document_content(aws, document) + + format_extensions = [ + ("markdown", "text/markdown", ".md"), + ("text", "text/plain", ".txt"), + ("html", "text/html", ".html"), + ("unknown", "text/plain", ".unknown"), # Default fallback + ] for ( target_format, diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py index 9b3b9807..276972a9 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py @@ -8,14 +8,13 @@ import pytest from moto import mock_aws from sqlmodel import Session -from tenacity import RetryError from app.crud import DocTransformationJobCrud -from app.core.doctransform.service import execute_job from app.models import Document, Project, TransformationStatus -from app.tests.core.doctransformer.test_service.base import ( - DocTransformTestBase, - MockHelpers, +from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase +from app.tests.core.doctransformer.test_service.utils import ( + create_failing_convert_document, + create_persistent_failing_convert_document, ) @@ -33,7 +32,7 @@ def test_execute_job_with_storage_error( """Test job execution when S3 upload fails.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) @@ -77,16 +76,14 @@ def test_execute_job_retry_mechanism( """Test that retry mechanism works for transient failures.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) db.commit() # Create a side effect that fails once then succeeds (fast retry will only try 2 times) - failing_convert_document = MockHelpers.create_failing_convert_document( - fail_count=1 - ) + failing_convert_document = create_failing_convert_document(fail_count=1) with patch( "app.core.doctransform.service.Session" @@ -119,7 +116,7 @@ def test_execute_job_exhausted_retries( """Test behavior when all retry attempts are exhausted.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) @@ -127,7 +124,7 @@ def test_execute_job_exhausted_retries( # Mock convert_document to always fail persistent_failing_convert_document = ( - MockHelpers.create_persistent_failing_convert_document("Persistent error") + create_persistent_failing_convert_document("Persistent error") ) with patch( @@ -163,7 +160,7 @@ def test_execute_job_database_error_during_completion( """Test handling of database errors when updating job completion.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) job_crud = DocTransformationJobCrud(session=db, project_id=project.id) job = job_crud.create(source_document_id=document.id) diff --git a/backend/app/tests/core/doctransformer/test_service/test_integration.py b/backend/app/tests/core/doctransformer/test_service/test_integration.py index 76f015e5..c856ebfa 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_integration.py +++ b/backend/app/tests/core/doctransformer/test_service/test_integration.py @@ -18,7 +18,7 @@ TransformationStatus, UserProjectOrg, ) -from app.tests.core.doctransformer.test_service.base import DocTransformTestBase +from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase class TestExecuteJobIntegration(DocTransformTestBase): @@ -32,7 +32,7 @@ def test_execute_job_end_to_end_workflow( """Test complete end-to-end workflow from start_job to execute_job.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) # Start job using the service current_user = UserProjectOrg( @@ -87,7 +87,7 @@ def test_execute_job_concurrent_jobs( """Test multiple concurrent job executions don't interfere with each other.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) # Create multiple jobs job_crud = DocTransformationJobCrud(session=db, project_id=project.id) @@ -124,7 +124,7 @@ def test_multiple_format_transformations( """Test transforming the same document to multiple formats.""" document, project = test_document aws = self.setup_aws_s3() - self.create_s3_document_content(aws, project, document) + self.create_s3_document_content(aws, document) formats = ["markdown", "text", "html"] jobs = [] diff --git a/backend/app/tests/core/doctransformer/test_service/test_start_job.py b/backend/app/tests/core/doctransformer/test_service/test_start_job.py index 18bd5c28..4539823b 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_start_job.py +++ b/backend/app/tests/core/doctransformer/test_service/test_start_job.py @@ -1,7 +1,7 @@ """ Tests for the start_job function in document transformation service. """ -from typing import Any, Tuple +from typing import Tuple from uuid import uuid4 import pytest @@ -17,10 +17,7 @@ TransformationStatus, UserProjectOrg, ) -from app.tests.core.doctransformer.test_service.base import ( - DocTransformTestBase, - TestDataProvider, -) +from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase class TestStartJob(DocTransformTestBase): @@ -136,9 +133,7 @@ def test_start_job_with_different_formats( task = background_tasks.tasks[-1] assert task.args[3] == target_format - @pytest.mark.parametrize( - "transformer_name", TestDataProvider.get_test_transformer_names() - ) + @pytest.mark.parametrize("transformer_name", ["test"]) def test_start_job_with_different_transformers( self, db: Session, diff --git a/backend/app/tests/core/doctransformer/test_service/utils.py b/backend/app/tests/core/doctransformer/test_service/utils.py new file mode 100644 index 00000000..5c1d0404 --- /dev/null +++ b/backend/app/tests/core/doctransformer/test_service/utils.py @@ -0,0 +1,82 @@ +""" +Base test class for DocTransform service tests. + +This module contains DocTransformTestBase with common AWS S3 setup and utilities. +All fixtures are automatically available from conftest.py in the same directory. +""" +from urllib.parse import urlparse + +from app.core.cloud import AmazonCloudStorageClient +from app.core.config import settings +from app.models import Document + + +class DocTransformTestBase: + """Base class for document transformation tests with common setup and utilities.""" + + def setup_aws_s3(self) -> AmazonCloudStorageClient: + """Setup AWS S3 for testing.""" + aws = AmazonCloudStorageClient() + aws.create() + return aws + + def create_s3_document_content( + self, + aws: AmazonCloudStorageClient, + document: Document, + content: bytes = b"Test document content", + ) -> bytes: + """Create content in S3 for a document.""" + parsed_url = urlparse(document.object_store_url) + s3_key = parsed_url.path.lstrip("/") + + aws.client.put_object(Bucket=settings.AWS_S3_BUCKET, Key=s3_key, Body=content) + return content + + def verify_s3_content( + self, + aws: AmazonCloudStorageClient, + transformed_doc: Document, + expected_content: str = None, + ) -> None: + """Verify the content stored in S3.""" + if expected_content is None: + expected_content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + + parsed_url = urlparse(transformed_doc.object_store_url) + transformed_key = parsed_url.path.lstrip("/") + + response = aws.client.get_object( + Bucket=settings.AWS_S3_BUCKET, Key=transformed_key + ) + transformed_content = response["Body"].read().decode("utf-8") + assert transformed_content == expected_content + + +def create_failing_convert_document(fail_count: int = 1): + """Create a side effect function that fails specified times then succeeds.""" + call_count = 0 + + def failing_convert_document(*args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count <= fail_count: + raise Exception("Transient error") + output_path = args[1] if len(args) > 1 else kwargs.get("output_path") + if output_path: + output_path.write_text("Success after retries", encoding="utf-8") + return output_path + raise ValueError("output_path is required") + + return failing_convert_document + + +def create_persistent_failing_convert_document( + error_message: str = "Persistent error", +): + """Create a side effect function that always fails.""" + + def persistent_failing_convert_document(*args, **kwargs): + raise Exception(error_message) + + return persistent_failing_convert_document \ No newline at end of file From de4a89db062452ffb85013395afdfd12dd0f9487 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:21:49 +0530 Subject: [PATCH 08/13] pre commit --- backend/app/tests/core/doctransformer/test_service/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/tests/core/doctransformer/test_service/utils.py b/backend/app/tests/core/doctransformer/test_service/utils.py index 5c1d0404..d240044f 100644 --- a/backend/app/tests/core/doctransformer/test_service/utils.py +++ b/backend/app/tests/core/doctransformer/test_service/utils.py @@ -79,4 +79,4 @@ def create_persistent_failing_convert_document( def persistent_failing_convert_document(*args, **kwargs): raise Exception(error_message) - return persistent_failing_convert_document \ No newline at end of file + return persistent_failing_convert_document From f12cba077f385f19fc389d6a5b1c097ff1b6bf48 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:54:47 +0530 Subject: [PATCH 09/13] move test transformer to test cases --- backend/app/core/doctransform/registry.py | 2 - .../app/core/doctransform/test_transformer.py | 16 -------- .../test_service/test_execute_job.py | 40 +++++++++++++++---- .../test_service/test_execute_job_errors.py | 19 +++++++-- .../test_service/test_integration.py | 25 ++++++++++-- .../test_service/test_start_job.py | 14 ++++++- .../core/doctransformer/test_service/utils.py | 16 ++++++++ 7 files changed, 99 insertions(+), 33 deletions(-) delete mode 100644 backend/app/core/doctransform/test_transformer.py diff --git a/backend/app/core/doctransform/registry.py b/backend/app/core/doctransform/registry.py index 39cfedd6..62c8c743 100644 --- a/backend/app/core/doctransform/registry.py +++ b/backend/app/core/doctransform/registry.py @@ -2,7 +2,6 @@ from typing import Type, Dict, Set, Tuple, Optional from app.core.doctransform.transformer import Transformer -from app.core.doctransform.test_transformer import TestTransformer from app.core.doctransform.zerox_transformer import ZeroxTransformer @@ -13,7 +12,6 @@ class TransformationError(Exception): # Map transformer names to their classes TRANSFORMERS: Dict[str, Type[Transformer]] = { "default": ZeroxTransformer, - "test": TestTransformer, "zerox": ZeroxTransformer, } diff --git a/backend/app/core/doctransform/test_transformer.py b/backend/app/core/doctransform/test_transformer.py deleted file mode 100644 index 28370525..00000000 --- a/backend/app/core/doctransform/test_transformer.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path -from app.core.doctransform.transformer import Transformer - - -class TestTransformer(Transformer): - """ - A test transformer that returns a hardcoded lorem ipsum string. - """ - - def transform(self, input_path: Path, output_path: Path) -> Path: - content = ( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " - "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - ) - output_path.write_text(content, encoding="utf-8") - return output_path diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py index 14aded18..5f80644a 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_execute_job.py +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job.py @@ -15,7 +15,10 @@ from app.core.doctransform.service import execute_job from app.core.exception_handlers import HTTPException from app.models import Document, Project, TransformationStatus -from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase +from app.tests.core.doctransformer.test_service.utils import ( + DocTransformTestBase, + MockTestTransformer, +) class TestExecuteJob(DocTransformTestBase): @@ -51,7 +54,11 @@ def test_execute_job_success( db.commit() # Mock the Session to use our existing database session - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -93,7 +100,11 @@ def test_execute_job_with_nonexistent_job( self.setup_aws_s3() nonexistent_job_id = uuid4() - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -123,7 +134,11 @@ def test_execute_job_with_missing_source_document( job = job_crud.create(source_document_id=document.id) db.commit() - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -163,7 +178,9 @@ def test_execute_job_with_transformer_error( "app.core.doctransform.service.Session" ) as mock_session_class, patch( "app.core.doctransform.service.convert_document" - ) as mock_convert: + ) as mock_convert, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None mock_convert.side_effect = TransformationError("Mock transformation error") @@ -198,7 +215,11 @@ def test_execute_job_status_transitions( initial_status = job.status db.commit() - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -240,7 +261,12 @@ def test_execute_job_with_different_content_types( job = job_crud.create(source_document_id=document.id) db.commit() - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", + {"test": MockTestTransformer}, + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None diff --git a/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py index 276972a9..e719cd5c 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py +++ b/backend/app/tests/core/doctransformer/test_service/test_execute_job_errors.py @@ -11,7 +11,10 @@ from app.crud import DocTransformationJobCrud from app.models import Document, Project, TransformationStatus -from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase +from app.tests.core.doctransformer.test_service.utils import ( + DocTransformTestBase, + MockTestTransformer, +) from app.tests.core.doctransformer.test_service.utils import ( create_failing_convert_document, create_persistent_failing_convert_document, @@ -43,7 +46,9 @@ def test_execute_job_with_storage_error( "app.core.doctransform.service.Session" ) as mock_session_class, patch( "app.core.doctransform.service.get_cloud_storage" - ) as mock_storage_class: + ) as mock_storage_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -90,6 +95,8 @@ def test_execute_job_retry_mechanism( ) as mock_session_class, patch( "app.core.doctransform.service.convert_document", side_effect=failing_convert_document, + ), patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -132,6 +139,8 @@ def test_execute_job_exhausted_retries( ) as mock_session_class, patch( "app.core.doctransform.service.convert_document", side_effect=persistent_failing_convert_document, + ), patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -166,7 +175,11 @@ def test_execute_job_database_error_during_completion( job = job_crud.create(source_document_id=document.id) db.commit() - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None diff --git a/backend/app/tests/core/doctransformer/test_service/test_integration.py b/backend/app/tests/core/doctransformer/test_service/test_integration.py index c856ebfa..20df4086 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_integration.py +++ b/backend/app/tests/core/doctransformer/test_service/test_integration.py @@ -18,7 +18,10 @@ TransformationStatus, UserProjectOrg, ) -from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase +from app.tests.core.doctransformer.test_service.utils import ( + DocTransformTestBase, + MockTestTransformer, +) class TestExecuteJobIntegration(DocTransformTestBase): @@ -57,7 +60,11 @@ def test_execute_job_end_to_end_workflow( assert job.status == TransformationStatus.PENDING # Execute the job manually (simulating background execution) - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", {"test": MockTestTransformer} + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -99,7 +106,12 @@ def test_execute_job_concurrent_jobs( # Execute all jobs for job in jobs: - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", + {"test": MockTestTransformer}, + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None @@ -138,7 +150,12 @@ def test_multiple_format_transformations( # Execute all jobs for job, target_format in jobs: - with patch("app.core.doctransform.service.Session") as mock_session_class: + with patch( + "app.core.doctransform.service.Session" + ) as mock_session_class, patch( + "app.core.doctransform.registry.TRANSFORMERS", + {"test": MockTestTransformer}, + ): mock_session_class.return_value.__enter__.return_value = db mock_session_class.return_value.__exit__.return_value = None diff --git a/backend/app/tests/core/doctransformer/test_service/test_start_job.py b/backend/app/tests/core/doctransformer/test_service/test_start_job.py index 4539823b..94a59dd1 100644 --- a/backend/app/tests/core/doctransformer/test_service/test_start_job.py +++ b/backend/app/tests/core/doctransformer/test_service/test_start_job.py @@ -9,6 +9,7 @@ from sqlmodel import Session from app.core.doctransform.service import execute_job, start_job +from app.core.doctransform.registry import TRANSFORMERS from app.core.exception_handlers import HTTPException from app.models import ( Document, @@ -17,7 +18,10 @@ TransformationStatus, UserProjectOrg, ) -from app.tests.core.doctransformer.test_service.utils import DocTransformTestBase +from app.tests.core.doctransformer.test_service.utils import ( + DocTransformTestBase, + MockTestTransformer, +) class TestStartJob(DocTransformTestBase): @@ -111,8 +115,12 @@ def test_start_job_with_different_formats( current_user: UserProjectOrg, test_document: Tuple[Document, Project], background_tasks: BackgroundTasks, + monkeypatch, ) -> None: """Test job creation with different target formats.""" + # Add the test transformer to the registry for this test + monkeypatch.setitem(TRANSFORMERS, "test", MockTestTransformer) + document, _ = test_document formats = ["markdown", "text", "html"] @@ -141,8 +149,12 @@ def test_start_job_with_different_transformers( test_document: Tuple[Document, Project], background_tasks: BackgroundTasks, transformer_name: str, + monkeypatch, ) -> None: """Test job creation with different transformer names.""" + # Add the test transformer to the registry for this test + monkeypatch.setitem(TRANSFORMERS, "test", MockTestTransformer) + document, _ = test_document job_id = start_job( diff --git a/backend/app/tests/core/doctransformer/test_service/utils.py b/backend/app/tests/core/doctransformer/test_service/utils.py index d240044f..82451add 100644 --- a/backend/app/tests/core/doctransformer/test_service/utils.py +++ b/backend/app/tests/core/doctransformer/test_service/utils.py @@ -4,13 +4,29 @@ This module contains DocTransformTestBase with common AWS S3 setup and utilities. All fixtures are automatically available from conftest.py in the same directory. """ +from pathlib import Path from urllib.parse import urlparse from app.core.cloud import AmazonCloudStorageClient from app.core.config import settings +from app.core.doctransform.transformer import Transformer from app.models import Document +class MockTestTransformer(Transformer): + """ + A mock transformer for testing that returns a hardcoded lorem ipsum string. + """ + + def transform(self, input_path: Path, output_path: Path) -> Path: + content = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + ) + output_path.write_text(content, encoding="utf-8") + return output_path + + class DocTransformTestBase: """Base class for document transformation tests with common setup and utilities.""" From 6461d5152c6ed44ac91f2f9abfbec00b02b22cf4 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:35:51 +0530 Subject: [PATCH 10/13] using PEP 585 style typing --- backend/app/core/doctransform/registry.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/app/core/doctransform/registry.py b/backend/app/core/doctransform/registry.py index 62c8c743..839cc876 100644 --- a/backend/app/core/doctransform/registry.py +++ b/backend/app/core/doctransform/registry.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Type, Dict, Set, Tuple, Optional from app.core.doctransform.transformer import Transformer from app.core.doctransform.zerox_transformer import ZeroxTransformer @@ -10,13 +9,13 @@ class TransformationError(Exception): # Map transformer names to their classes -TRANSFORMERS: Dict[str, Type[Transformer]] = { +TRANSFORMERS: dict[str, type[Transformer]] = { "default": ZeroxTransformer, "zerox": ZeroxTransformer, } # Define supported transformations: (source_format, target_format) -> [available_transformers] -SUPPORTED_TRANSFORMATIONS: Dict[Tuple[str, str], Dict[str, str]] = { +SUPPORTED_TRANSFORMATIONS: dict[tuple[str, str], dict[str, str]] = { ("pdf", "markdown"): { "default": "zerox", "zerox": "zerox", @@ -27,7 +26,7 @@ class TransformationError(Exception): } # Map file extensions to format names -EXTENSION_TO_FORMAT: Dict[str, str] = { +EXTENSION_TO_FORMAT: dict[str, str] = { ".pdf": "pdf", ".docx": "docx", ".doc": "doc", @@ -39,7 +38,7 @@ class TransformationError(Exception): } # Map format names to file extensions -FORMAT_TO_EXTENSION: Dict[str, str] = { +FORMAT_TO_EXTENSION: dict[str, str] = { "pdf": ".pdf", "docx": ".docx", "doc": ".doc", @@ -58,7 +57,7 @@ def get_file_format(filename: str) -> str: return format_name -def get_supported_transformations() -> Dict[Tuple[str, str], Set[str]]: +def get_supported_transformations() -> dict[tuple[str, str], set[str]]: """Get all supported transformation combinations.""" return { key: set(transformers.keys()) @@ -73,13 +72,13 @@ def is_transformation_supported(source_format: str, target_format: str) -> bool: def get_available_transformers( source_format: str, target_format: str -) -> Dict[str, str]: +) -> dict[str, str]: """Get available transformers for a specific transformation.""" return SUPPORTED_TRANSFORMATIONS.get((source_format, target_format), {}) def resolve_transformer( - source_format: str, target_format: str, transformer_name: Optional[str] = None + source_format: str, target_format: str, transformer_name: str | None = None ) -> str: """ Resolve the actual transformer to use for a transformation. From 9f68c0f754c89914782cdaec1a653959bd25d257 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:43:39 +0530 Subject: [PATCH 11/13] add a timeout error to transformer --- .../core/doctransform/zerox_transformer.py | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/backend/app/core/doctransform/zerox_transformer.py b/backend/app/core/doctransform/zerox_transformer.py index 141e7164..a69f3147 100644 --- a/backend/app/core/doctransform/zerox_transformer.py +++ b/backend/app/core/doctransform/zerox_transformer.py @@ -1,9 +1,11 @@ -from asyncio import Runner import logging + +from asyncio import Runner, wait_for from pathlib import Path -from app.core.doctransform.transformer import Transformer from pyzerox import zerox +from app.core.doctransform.transformer import Transformer + logger = logging.getLogger(__name__) @@ -23,27 +25,21 @@ def transform(self, input_path: Path, output_path: Path) -> Path: try: with Runner() as runner: result = runner.run( - zerox( - file_path=str(input_path), - model=self.model, + wait_for( + zerox( + file_path=str(input_path), + model=self.model, + ), + timeout=10 * 60, # 10 minutes ) ) - if result is None or not hasattr(result, "pages") or result.pages is None: - raise RuntimeError( - "Zerox returned no pages. This may indicate a PDF/image conversion failure (is Poppler installed and in PATH?)" - ) - - with output_path.open("w", encoding="utf-8") as output_file: - for page in result.pages: - if not getattr(page, "content", None): - continue - output_file.write(page.content) - output_file.write("\n\n") - - logger.info( - f"[ZeroxTransformer.transform] Transformation completed, output written to: {output_path}" + except TimeoutError: + logger.error( + f"ZeroxTransformer timed out for {input_path} (model={self.model})" + ) + raise RuntimeError( + f"ZeroxTransformer PDF extraction timed out after {10*60} seconds for {input_path}" ) - return output_path except Exception as e: logger.error( f"ZeroxTransformer failed for {input_path}: {e}\n" @@ -54,3 +50,21 @@ def transform(self, input_path: Path, output_path: Path) -> Path: f"Failed to extract content from PDF. " f"Check that Poppler is installed and in your PATH. Original error: {e}" ) from e + + if result is None or not hasattr(result, "pages") or result.pages is None: + raise RuntimeError( + "Zerox returned no pages. This may indicate a PDF/image conversion failure " + "(is Poppler installed and in PATH?)" + ) + + with output_path.open("w", encoding="utf-8") as output_file: + for page in result.pages: + if not getattr(page, "content", None): + continue + output_file.write(page.content) + output_file.write("\n\n") + + logger.info( + f"[ZeroxTransformer.transform] Transformation completed, output written to: {output_path}" + ) + return output_path From 76f8757ba95be1a52f57b0694231179aea97ad40 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:56:13 +0530 Subject: [PATCH 12/13] take job_ids as list[uuid] instead of str --- .../app/api/routes/doc_transformation_job.py | 27 ++---------- .../api/routes/test_doc_transformation_job.py | 42 +++++++++---------- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/backend/app/api/routes/doc_transformation_job.py b/backend/app/api/routes/doc_transformation_job.py index c66ad910..8bf29ad1 100644 --- a/backend/app/api/routes/doc_transformation_job.py +++ b/backend/app/api/routes/doc_transformation_job.py @@ -33,30 +33,11 @@ def get_transformation_job( def get_multiple_transformation_jobs( session: SessionDep, current_user: CurrentUserOrgProject, - job_ids: str = Query( - ..., description="Comma-separated list of transformation job IDs" - ), + job_ids: list[UUID] = Query(description="List of transformation job IDs"), ): - job_id_list = [] - invalid_ids = [] - for jid in job_ids.split(","): - jid = jid.strip() - if not jid: - continue - try: - job_id_list.append(UUID(jid)) - except ValueError: - invalid_ids.append(jid) - - if invalid_ids: - raise HTTPException( - status_code=422, - detail=f"Invalid UUID(s) provided: {', '.join(invalid_ids)}", - ) - crud = DocTransformationJobCrud(session, project_id=current_user.project_id) - jobs = crud.read_each(set(job_id_list)) - jobs_not_found = set(job_id_list) - {job.id for job in jobs} + jobs = crud.read_each(set(job_ids)) + jobs_not_found = set(job_ids) - {job.id for job in jobs} return APIResponse.success_response( - DocTransformationJobs(jobs=jobs, jobs_not_found=jobs_not_found) + DocTransformationJobs(jobs=jobs, jobs_not_found=list(jobs_not_found)) ) diff --git a/backend/app/tests/api/routes/test_doc_transformation_job.py b/backend/app/tests/api/routes/test_doc_transformation_job.py index 7eeb7dee..a808c430 100644 --- a/backend/app/tests/api/routes/test_doc_transformation_job.py +++ b/backend/app/tests/api/routes/test_doc_transformation_job.py @@ -137,10 +137,10 @@ def test_get_multiple_jobs_success( crud = DocTransformationJobCrud(db, user_api_key.project_id) documents = store.fill(3) jobs = [crud.create(doc.id) for doc in documents] - job_ids_str = ",".join(str(job.id) for job in jobs) + job_ids_params = "&".join(f"job_ids={job.id}" for job in jobs) response = client.get( - f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + f"{settings.API_V1_STR}/documents/transformations/?{job_ids_params}", headers={"X-API-KEY": user_api_key.key}, ) @@ -164,10 +164,12 @@ def test_get_mixed_existing_nonexisting_jobs( jobs = [crud.create(doc.id) for doc in documents] fake_uuid = "00000000-0000-0000-0000-000000000001" - job_ids_str = f"{jobs[0].id},{jobs[1].id},{fake_uuid}" + job_ids_params = ( + f"job_ids={jobs[0].id}&job_ids={jobs[1].id}&job_ids={fake_uuid}" + ) response = client.get( - f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + f"{settings.API_V1_STR}/documents/transformations/?{job_ids_params}", headers={"X-API-KEY": user_api_key.key}, ) @@ -186,39 +188,33 @@ def test_get_jobs_with_empty_string( headers={"X-API-KEY": user_api_key.key}, ) - assert response.status_code == 200 - data = response.json() - assert len(data["data"]["jobs"]) == 0 - assert len(data["data"]["jobs_not_found"]) == 0 + assert response.status_code == 422 def test_get_jobs_with_whitespace_only( self, client: TestClient, user_api_key: APIKeyPublic ): """Test retrieving jobs with whitespace-only job_ids.""" response = client.get( - f"{settings.API_V1_STR}/documents/transformations/?job_ids= , , ", + f"{settings.API_V1_STR}/documents/transformations/?job_ids= ", headers={"X-API-KEY": user_api_key.key}, ) - assert response.status_code == 200 - data = response.json() - assert len(data["data"]["jobs"]) == 0 - assert len(data["data"]["jobs_not_found"]) == 0 + assert response.status_code == 422 def test_get_jobs_invalid_uuid_format_422( self, client: TestClient, user_api_key: APIKeyPublic ): """Test that invalid UUID format returns 422.""" - invalid_uuids = "not-a-uuid,also-not-uuid" + invalid_uuid = "not-a-uuid" response = client.get( - f"{settings.API_V1_STR}/documents/transformations/?job_ids={invalid_uuids}", + f"{settings.API_V1_STR}/documents/transformations/?job_ids={invalid_uuid}", headers={"X-API-KEY": user_api_key.key}, ) assert response.status_code == 422 data = response.json() - assert "Invalid UUID(s) provided" in data["error"] + assert "Input should be a valid UUID" in data["error"] def test_get_jobs_mixed_valid_invalid_uuid_422( self, client: TestClient, db: Session, user_api_key: APIKeyPublic @@ -229,22 +225,22 @@ def test_get_jobs_mixed_valid_invalid_uuid_422( document = store.put() job = crud.create(document.id) - job_ids_str = f"{job.id},not-a-uuid" + job_ids_params = f"job_ids={job.id}&job_ids=not-a-uuid" response = client.get( - f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + f"{settings.API_V1_STR}/documents/transformations/?{job_ids_params}", headers={"X-API-KEY": user_api_key.key}, ) assert response.status_code == 422 data = response.json() - assert "Invalid UUID(s) provided" in data["error"] - assert "not-a-uuid" in data["error"] + assert "Input should be a valid UUID" in data["error"] + assert "job_ids" in data["error"] def test_get_jobs_missing_parameter_422( self, client: TestClient, user_api_key: APIKeyPublic ): - """Test that missing job_ids parameter returns 422.""" + """Test that missing job_ids parameter returns empty results.""" response = client.get( f"{settings.API_V1_STR}/documents/transformations/", headers={"X-API-KEY": user_api_key.key}, @@ -296,10 +292,10 @@ def test_get_jobs_with_various_statuses( jobs[3].id, TransformationStatus.FAILED, error_message="Test error" ) - job_ids_str = ",".join(str(job.id) for job in jobs) + job_ids_params = "&".join(f"job_ids={job.id}" for job in jobs) response = client.get( - f"{settings.API_V1_STR}/documents/transformations/?job_ids={job_ids_str}", + f"{settings.API_V1_STR}/documents/transformations/?{job_ids_params}", headers={"X-API-KEY": user_api_key.key}, ) From e06042fcde32d328af0d909e2fe2a3f2c57cf503 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:08:42 +0530 Subject: [PATCH 13/13] add limit on no. of job ids can be added --- backend/app/api/routes/doc_transformation_job.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/doc_transformation_job.py b/backend/app/api/routes/doc_transformation_job.py index 8bf29ad1..fa40769b 100644 --- a/backend/app/api/routes/doc_transformation_job.py +++ b/backend/app/api/routes/doc_transformation_job.py @@ -33,7 +33,9 @@ def get_transformation_job( def get_multiple_transformation_jobs( session: SessionDep, current_user: CurrentUserOrgProject, - job_ids: list[UUID] = Query(description="List of transformation job IDs"), + job_ids: list[UUID] = Query( + description="List of transformation job IDs", min=1, max_length=100 + ), ): crud = DocTransformationJobCrud(session, project_id=current_user.project_id) jobs = crud.read_each(set(job_ids))