From f16eefc5846337dd317d07a00ec7ab37df70d31a Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:17:07 +0300 Subject: [PATCH 01/81] feat(model): add document table model --- src/ai_notes_api/db/models/__init__.py | 3 + src/ai_notes_api/db/models/chat_session.py | 17 ++- src/ai_notes_api/db/models/document.py | 124 +++++++++++++++++++++ 3 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 src/ai_notes_api/db/models/document.py diff --git a/src/ai_notes_api/db/models/__init__.py b/src/ai_notes_api/db/models/__init__.py index ad79358..81c906e 100644 --- a/src/ai_notes_api/db/models/__init__.py +++ b/src/ai_notes_api/db/models/__init__.py @@ -7,6 +7,7 @@ from .chat_memory import ChatMemory from .chat_session import ChatSession, ChatSessionGenerationStatus from .datetime import SoftDeleteMixin, TimestampMixin +from .document import Document, DocumentStatus from .generation_job import GenerationJob, GenerationJobStatus from .message import Message, MessageRole from .note import ModelSource, Note @@ -26,4 +27,6 @@ "GenerationJobStatus", "ChatSessionGenerationStatus", "ChatMemory", + "Document", + "DocumentStatus", ] diff --git a/src/ai_notes_api/db/models/chat_session.py b/src/ai_notes_api/db/models/chat_session.py index 8bf4bf8..58be3a9 100644 --- a/src/ai_notes_api/db/models/chat_session.py +++ b/src/ai_notes_api/db/models/chat_session.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_memory import ChatMemory + from ai_notes_api.db.models.document import Document from ai_notes_api.db.models.generation_job import GenerationJob from ai_notes_api.db.models.message import Message from ai_notes_api.db.models.user import User @@ -39,20 +40,19 @@ class ChatSession(Base, TimestampMixin, SoftDeleteMixin): Attributes: id (Mapped[UUID]): Unique chat session identifier. - user_id (Mapped[UUID]): Identifier of the user who owns the chat - session. + user_id (Mapped[UUID]): Identifier of the user who owns the chat session. user (Mapped[User]): User who owns the chat session. title (Mapped[str]): Chat session title. generation_status (Mapped[ChatSessionGenerationStatus]): Current LLM generation status for the chat session. - generation_id (Mapped[UUID | None]): Optional active generation - identifier. + generation_id (Mapped[UUID | None]): Optional active generation identifier. generation_started_at (Mapped[datetime | None]): Date and time when the active generation started. - messages (Mapped[list[Message]]): Messages that belong to the chat - session. + messages (Mapped[list[Message]]): Messages that belong to the chat session. generation_jobs (Mapped[list[GenerationJob]]): Generation jobs that belong to the chat session. + memory (Mapped[ChatMemory]): Memory associated with the chat session. + documents (Mapped[list[Document]]): Documents that belong to the chat session. """ __tablename__ = "chat_sessions" @@ -121,3 +121,8 @@ class ChatSession(Base, TimestampMixin, SoftDeleteMixin): cascade="all, delete-orphan", uselist=False, ) + + documents: Mapped[list["Document"]] = relationship( + back_populates="documents", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/document.py b/src/ai_notes_api/db/models/document.py new file mode 100644 index 0000000..ef842fa --- /dev/null +++ b/src/ai_notes_api/db/models/document.py @@ -0,0 +1,124 @@ +"""Document database model module. + +This module defines the SQLAlchemy ORM model for chat documents and the enum +used to track document processing status. +""" + +from enum import StrEnum +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from sqlalchemy import Enum as SqlEnum +from sqlalchemy import ForeignKey, String, Text, Uuid +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from ai_notes_api.db.models.base import Base +from ai_notes_api.db.models.datetime import SoftDeleteMixin, TimestampMixin + +if TYPE_CHECKING: + from ai_notes_api.db.models.chat_session import ChatSession + + +class DocumentStatus(StrEnum): + """Processing status of a chat document. + + Attributes: + UPLOADED (str): Document has been uploaded but not yet processed. + PROCESSING (str): Document is currently being processed. + READY (str): Document was processed successfully and is ready for use. + FAILED (str): Document processing failed. + DELETED (str): Document was deleted. + """ + + UPLOADED = "uploaded" + PROCESSING = "processing" + READY = "ready" + FAILED = "failed" + DELETED = "deleted" + + +class Document(Base, TimestampMixin, SoftDeleteMixin): + """SQLAlchemy ORM model representing a chat document. + + Attributes: + id (Mapped[UUID]): Unique document identifier. + session_id (Mapped[UUID]): Identifier of the chat session that owns the + document. + chat_session (Mapped[ChatSession]): Chat session that owns the document. + filename (Mapped[str]): Original document file name. + content_type (Mapped[str]): MIME type of the document. + file_size (Mapped[int]): Document size in bytes. + checksum_sha256 (Mapped[str]): SHA-256 checksum of the document content. + storage_bucket (Mapped[str]): Storage bucket where the document is + stored. + storage_object_name (Mapped[str]): Object name of the document within + the storage bucket. + status (Mapped[DocumentStatus]): Current document processing status. + error_message (Mapped[str | None]): Optional error message if document + processing failed. + """ + + __tablename__ = "documents" + + id: Mapped[UUID] = mapped_column( + Uuid, + primary_key=True, + default=uuid4, + ) + + session_id: Mapped[UUID] = mapped_column( + ForeignKey( + "documents.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + chat_session: Mapped["ChatSession"] = relationship( + back_populates="documents", + ) + + filename: Mapped[str] = mapped_column( + String(255), + nullable=False, + ) + + content_type: Mapped[str] = mapped_column( + String(255), + nullable=False, + ) + + file_size: Mapped[int] = mapped_column( + nullable=False, + ) + + checksum_sha256: Mapped[str] = mapped_column( + String(255), + nullable=False, + ) + + storage_bucket: Mapped[str] = mapped_column( + String(255), + nullable=False, + ) + + storage_object_name: Mapped[str] = mapped_column( + String(255), + nullable=False, + ) + + status: Mapped[DocumentStatus] = mapped_column( + SqlEnum( + DocumentStatus, + name="document_status", + values_callable=lambda enum_cls: [item.value for item in enum_cls], + ), + nullable=False, + ) + + error_message: Mapped[str | None] = mapped_column( + Text, + default=None, + nullable=True, + ) From a97f9d20999d1e9e4a7de95a4e966ee3d8628eab Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:21:14 +0300 Subject: [PATCH 02/81] build(alembic): add documents table --- .../683f77be6a55_add_documents_table.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 alembic/versions/683f77be6a55_add_documents_table.py diff --git a/alembic/versions/683f77be6a55_add_documents_table.py b/alembic/versions/683f77be6a55_add_documents_table.py new file mode 100644 index 0000000..314d321 --- /dev/null +++ b/alembic/versions/683f77be6a55_add_documents_table.py @@ -0,0 +1,52 @@ +"""'Add documents table' + +Revision ID: 683f77be6a55 +Revises: 93228745b965 +Create Date: 2026-06-23 05:20:47.482480 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '683f77be6a55' +down_revision: Union[str, Sequence[str], None] = '93228745b965' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('documents', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('session_id', sa.Uuid(), nullable=False), + sa.Column('filename', sa.String(length=255), nullable=False), + sa.Column('content_type', sa.String(length=255), nullable=False), + sa.Column('file_size', sa.Integer(), nullable=False), + sa.Column('checksum_sha256', sa.String(length=255), nullable=False), + sa.Column('storage_bucket', sa.String(length=255), nullable=False), + sa.Column('storage_object_name', sa.String(length=255), nullable=False), + sa.Column('status', sa.Enum('uploaded', 'processing', 'ready', 'failed', 'deleted', name='document_status'), nullable=False), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['session_id'], ['documents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_documents_session_id'), 'documents', ['session_id'], unique=False) + op.add_column('chat_memories', sa.Column('is_summarizing', sa.Boolean(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('chat_memories', 'is_summarizing') + op.drop_index(op.f('ix_documents_session_id'), table_name='documents') + op.drop_table('documents') + # ### end Alembic commands ### From 69258b47913f5a84cac27204921b9bcecc3593a9 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:22:57 +0300 Subject: [PATCH 03/81] build(alembic): enable pgvector extension --- .../722ede82be97_enable_pgvector_extension.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 alembic/versions/722ede82be97_enable_pgvector_extension.py diff --git a/alembic/versions/722ede82be97_enable_pgvector_extension.py b/alembic/versions/722ede82be97_enable_pgvector_extension.py new file mode 100644 index 0000000..2c72b53 --- /dev/null +++ b/alembic/versions/722ede82be97_enable_pgvector_extension.py @@ -0,0 +1,28 @@ +"""'Enable pgvector extension' + +Revision ID: 722ede82be97 +Revises: 683f77be6a55 +Create Date: 2026-06-23 05:21:20.142401 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '722ede82be97' +down_revision: Union[str, Sequence[str], None] = '683f77be6a55' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Enable the pgvector extension in PostgreSQL.""" + op.execute("CREATE EXTENSION IF NOT EXISTS vector") + + +def downgrade() -> None: + """Disable the pgvector extension in PostgreSQL.""" + op.execute("DROP EXTENSION IF EXISTS vector") From b3d59c2e2c26cffd175ecdda0c1a3c5cc9d35e81 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:23:11 +0300 Subject: [PATCH 04/81] build(docker): update postgres image --- compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index fcd9b6a..3fd0316 100644 --- a/compose.yml +++ b/compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres:16 + image: pgvector/pgvector:pg16 container_name: ai_notes_db restart: unless-stopped environment: From a836fe4ca2f3e1bcfe22e889cbc4ecf74137c2d4 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:32:17 +0300 Subject: [PATCH 05/81] feat(models): connect user to documents --- src/ai_notes_api/db/models/chat_session.py | 2 +- src/ai_notes_api/db/models/document.py | 21 ++++++++++++++++++--- src/ai_notes_api/db/models/user.py | 12 ++++++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/ai_notes_api/db/models/chat_session.py b/src/ai_notes_api/db/models/chat_session.py index 58be3a9..3d94abf 100644 --- a/src/ai_notes_api/db/models/chat_session.py +++ b/src/ai_notes_api/db/models/chat_session.py @@ -123,6 +123,6 @@ class ChatSession(Base, TimestampMixin, SoftDeleteMixin): ) documents: Mapped[list["Document"]] = relationship( - back_populates="documents", + back_populates="chat_session", cascade="all, delete-orphan", ) diff --git a/src/ai_notes_api/db/models/document.py b/src/ai_notes_api/db/models/document.py index ef842fa..cfab412 100644 --- a/src/ai_notes_api/db/models/document.py +++ b/src/ai_notes_api/db/models/document.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession + from ai_notes_api.db.models.user import User class DocumentStatus(StrEnum): @@ -42,6 +43,8 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): Attributes: id (Mapped[UUID]): Unique document identifier. + user_id (Mapped[UUID]): Identifier of the user who owns the document. + user (Mapped[User]): User who owns the document. session_id (Mapped[UUID]): Identifier of the chat session that owns the document. chat_session (Mapped[ChatSession]): Chat session that owns the document. @@ -49,8 +52,7 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): content_type (Mapped[str]): MIME type of the document. file_size (Mapped[int]): Document size in bytes. checksum_sha256 (Mapped[str]): SHA-256 checksum of the document content. - storage_bucket (Mapped[str]): Storage bucket where the document is - stored. + storage_bucket (Mapped[str]): Storage bucket where the document is stored. storage_object_name (Mapped[str]): Object name of the document within the storage bucket. status (Mapped[DocumentStatus]): Current document processing status. @@ -66,9 +68,22 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): default=uuid4, ) + user_id: Mapped[UUID] = mapped_column( + ForeignKey( + "users.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + user: Mapped["User"] = relationship( + back_populates="documents", + ) + session_id: Mapped[UUID] = mapped_column( ForeignKey( - "documents.id", + "chat_sessions.id", ondelete="CASCADE", ), nullable=False, diff --git a/src/ai_notes_api/db/models/user.py b/src/ai_notes_api/db/models/user.py index f057d6d..444fbba 100644 --- a/src/ai_notes_api/db/models/user.py +++ b/src/ai_notes_api/db/models/user.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession + from ai_notes_api.db.models.document import Document from ai_notes_api.db.models.generation_job import GenerationJob from ai_notes_api.db.models.note import Note @@ -29,8 +30,10 @@ class User(Base, TimestampMixin): is_active (Mapped[bool]): Whether the user account is active. is_superuser (Mapped[bool]): Whether the user has superuser privileges. notes (Mapped[list[Note]]): Notes owned by the user. - chat_sessions (Mapped[list[ChatSession]]): Chat sessions owned by the - user. + chat_sessions (Mapped[list[ChatSession]]): Chat sessions owned by the user. + generation_jobs (Mapped[list[GenerationJob]]): Generation jobs owned by + the user. + documents (Mapped[list[Document]]): Documents owned by the user. """ __tablename__ = "users" @@ -79,3 +82,8 @@ class User(Base, TimestampMixin): back_populates="user", cascade="all, delete-orphan", ) + + documents: Mapped[list["Document"]] = relationship( + back_populates="user", + cascade="all, delete-orphan", + ) From 84e30bff3238bbd6fb04c433706b1522d80fdcee Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:32:32 +0300 Subject: [PATCH 06/81] build(alembic): connect user to documents --- ...ebb7e2_connect_users_table_to_documents.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 alembic/versions/692926ebb7e2_connect_users_table_to_documents.py diff --git a/alembic/versions/692926ebb7e2_connect_users_table_to_documents.py b/alembic/versions/692926ebb7e2_connect_users_table_to_documents.py new file mode 100644 index 0000000..ac7f0d7 --- /dev/null +++ b/alembic/versions/692926ebb7e2_connect_users_table_to_documents.py @@ -0,0 +1,40 @@ +"""'Connect users table to documents' + +Revision ID: 692926ebb7e2 +Revises: 722ede82be97 +Create Date: 2026-06-23 05:31:47.106206 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '692926ebb7e2' +down_revision: Union[str, Sequence[str], None] = '722ede82be97' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('documents', sa.Column('user_id', sa.Uuid(), nullable=False)) + op.create_index(op.f('ix_documents_user_id'), 'documents', ['user_id'], unique=False) + op.drop_constraint(op.f('documents_session_id_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(None, 'documents', 'chat_sessions', ['session_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'documents', type_='foreignkey') + op.drop_constraint(None, 'documents', type_='foreignkey') + op.create_foreign_key(op.f('documents_session_id_fkey'), 'documents', 'documents', ['session_id'], ['id'], ondelete='CASCADE') + op.drop_index(op.f('ix_documents_user_id'), table_name='documents') + op.drop_column('documents', 'user_id') + # ### end Alembic commands ### From 79d6fa7bfece5662f6ec3f251b1d2f7fb5b59fd4 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:43:45 +0300 Subject: [PATCH 07/81] feat(model): add document_chunk table --- pyproject.toml | 1 + src/ai_notes_api/db/models/chat_session.py | 8 +- src/ai_notes_api/db/models/document.py | 9 +- src/ai_notes_api/db/models/document_chunk.py | 121 +++++++++++++++++++ src/ai_notes_api/db/models/generation_job.py | 3 - src/ai_notes_api/db/models/user.py | 8 ++ uv.lock | 54 +++++++++ 7 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 src/ai_notes_api/db/models/document_chunk.py diff --git a/pyproject.toml b/pyproject.toml index 4ada250..3cd3787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "sse-starlette>=3.4.4", "celery>=5.6.3", "redis>=8.0.0", + "pgvector>=0.4.2", ] [dependency-groups] diff --git a/src/ai_notes_api/db/models/chat_session.py b/src/ai_notes_api/db/models/chat_session.py index 3d94abf..0ca0801 100644 --- a/src/ai_notes_api/db/models/chat_session.py +++ b/src/ai_notes_api/db/models/chat_session.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_memory import ChatMemory from ai_notes_api.db.models.document import Document + from ai_notes_api.db.models.document_chunk import DocumentChunk from ai_notes_api.db.models.generation_job import GenerationJob from ai_notes_api.db.models.message import Message from ai_notes_api.db.models.user import User @@ -95,14 +96,12 @@ class ChatSession(Base, TimestampMixin, SoftDeleteMixin): generation_id: Mapped[UUID | None] = mapped_column( Uuid, - default=None, nullable=True, index=True, ) generation_started_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), - default=None, nullable=True, ) @@ -126,3 +125,8 @@ class ChatSession(Base, TimestampMixin, SoftDeleteMixin): back_populates="chat_session", cascade="all, delete-orphan", ) + + document_chunks: Mapped[list["DocumentChunk"]] = relationship( + back_populates="chat_session", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/document.py b/src/ai_notes_api/db/models/document.py index cfab412..e289fd6 100644 --- a/src/ai_notes_api/db/models/document.py +++ b/src/ai_notes_api/db/models/document.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession + from ai_notes_api.db.models.document_chunk import DocumentChunk from ai_notes_api.db.models.user import User @@ -58,6 +59,8 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): status (Mapped[DocumentStatus]): Current document processing status. error_message (Mapped[str | None]): Optional error message if document processing failed. + document_chunks (Mapped[list[DocumentChunk]]): Chunks that belong to the + document. """ __tablename__ = "documents" @@ -134,6 +137,10 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): error_message: Mapped[str | None] = mapped_column( Text, - default=None, nullable=True, ) + + document_chunks: Mapped[list["DocumentChunk"]] = relationship( + back_populates="document", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/document_chunk.py b/src/ai_notes_api/db/models/document_chunk.py new file mode 100644 index 0000000..663cae3 --- /dev/null +++ b/src/ai_notes_api/db/models/document_chunk.py @@ -0,0 +1,121 @@ +"""Document chunk database model module. + +This module defines the SQLAlchemy ORM model for document chunks and their +vector embeddings used for semantic search. +""" + +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from pgvector.sqlalchemy import Vector +from sqlalchemy import ForeignKey, String, Text, Uuid +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from ai_notes_api.db.models.base import Base +from ai_notes_api.db.models.datetime import SoftDeleteMixin, TimestampMixin + +if TYPE_CHECKING: + from ai_notes_api.db.models.chat_session import ChatSession + from ai_notes_api.db.models.document import Document + from ai_notes_api.db.models.user import User + + +class DocumentChunk(Base, TimestampMixin, SoftDeleteMixin): + """SQLAlchemy ORM model representing a document chunk. + + Attributes: + id (Mapped[UUID]): Unique document chunk identifier. + user_id (Mapped[UUID]): Identifier of the user who owns the document + chunk. + user (Mapped[User]): User who owns the document chunk. + session_id (Mapped[UUID]): Identifier of the chat session that owns the + document chunk. + chat_session (Mapped[ChatSession]): Chat session that owns the document + chunk. + document_id (Mapped[UUID]): Identifier of the document the chunk belongs + to. + document (Mapped[Document]): Document the chunk belongs to. + chunk_index (Mapped[int]): Position of the chunk within the document. + content (Mapped[str]): Text content of the chunk. + content_hash (Mapped[str]): Hash of the chunk content. + embedding (Mapped[list[float]]): Vector embedding of the chunk content. + embedding_model (Mapped[str]): Name of the model used to produce the + embedding. + token_count (Mapped[int | None]): Optional number of tokens in the + chunk. + """ + + __tablename__ = "document_chunks" + + id: Mapped[UUID] = mapped_column( + Uuid, + primary_key=True, + default=uuid4, + ) + + user_id: Mapped[UUID] = mapped_column( + ForeignKey( + "users.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + user: Mapped["User"] = relationship( + back_populates="document_chunks", + ) + + session_id: Mapped[UUID] = mapped_column( + ForeignKey( + "chat_sessions.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + chat_session: Mapped["ChatSession"] = relationship( + back_populates="document_chunks", + ) + + document_id: Mapped[UUID] = mapped_column( + ForeignKey( + "documents.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + document: Mapped["Document"] = relationship( + back_populates="document_chunks", + ) + + chunk_index: Mapped[int] = mapped_column( + nullable=False, + ) + + content: Mapped[str] = mapped_column( + Text, + nullable=False, + ) + + content_hash: Mapped[str] = mapped_column( + String(255), + nullable=False, + ) + + embedding: Mapped[list[float]] = mapped_column( + Vector(1536), + nullable=False, + ) + + embedding_model: Mapped[str] = mapped_column( + String(255), + nullable=False, + ) + + token_count: Mapped[int | None] = mapped_column( + nullable=True, + ) diff --git a/src/ai_notes_api/db/models/generation_job.py b/src/ai_notes_api/db/models/generation_job.py index ed2fc6e..8d69713 100644 --- a/src/ai_notes_api/db/models/generation_job.py +++ b/src/ai_notes_api/db/models/generation_job.py @@ -122,18 +122,15 @@ class GenerationJob(Base, TimestampMixin): error: Mapped[str | None] = mapped_column( Text, - default=None, nullable=True, ) started_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), - default=None, nullable=True, ) finished_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), - default=None, nullable=True, ) diff --git a/src/ai_notes_api/db/models/user.py b/src/ai_notes_api/db/models/user.py index 444fbba..cb5576a 100644 --- a/src/ai_notes_api/db/models/user.py +++ b/src/ai_notes_api/db/models/user.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession from ai_notes_api.db.models.document import Document + from ai_notes_api.db.models.document_chunk import DocumentChunk from ai_notes_api.db.models.generation_job import GenerationJob from ai_notes_api.db.models.note import Note @@ -34,6 +35,8 @@ class User(Base, TimestampMixin): generation_jobs (Mapped[list[GenerationJob]]): Generation jobs owned by the user. documents (Mapped[list[Document]]): Documents owned by the user. + document_chunks (Mapped[list[DocumentChunk]]): Document chunks owned by + the user. """ __tablename__ = "users" @@ -87,3 +90,8 @@ class User(Base, TimestampMixin): back_populates="user", cascade="all, delete-orphan", ) + + document_chunks: Mapped[list["DocumentChunk"]] = relationship( + back_populates="user", + cascade="all, delete-orphan", + ) diff --git a/uv.lock b/uv.lock index 9324461..dc48fad 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,7 @@ dependencies = [ { name = "loguru" }, { name = "openai" }, { name = "passlib" }, + { name = "pgvector" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-settings" }, { name = "python-jose" }, @@ -59,6 +60,7 @@ requires-dist = [ { name = "loguru", specifier = ">=0.7.3" }, { name = "openai", specifier = ">=2.41.1" }, { name = "passlib", specifier = ">=1.7.4" }, + { name = "pgvector", specifier = ">=0.4.2" }, { name = "pydantic", extras = ["email"], specifier = ">=2.13.4" }, { name = "pydantic-settings", specifier = ">=2.14.1" }, { name = "python-jose", specifier = ">=3.5.0" }, @@ -1133,6 +1135,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "numpy" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/05/3d27272d30698dc0ecb7fdfaa41ad70303b444f81722bb99bce1d818638a/numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c", size = 20652461, upload-time = "2026-06-21T20:57:51.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/33/07675aaad7f26ea013d5e884d9a0d784b79c6bd7566c333f5a52fa3c610b/numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2", size = 16784890, upload-time = "2026-06-21T20:56:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c", size = 11754584, upload-time = "2026-06-21T20:56:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/44/9b/56dd530c367c74ae17411027cea4135ca57e1e0583bf5594cee18bd83217/numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd", size = 5313904, upload-time = "2026-06-21T20:56:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b0/bcd672edad27ecca7da1f7bb0ce72cd1706a4f2d79ae94990afc97c13e1c/numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd", size = 6648504, upload-time = "2026-06-21T20:56:47.567Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/15cdfcbd30a1544a46c9e487a00df331c4672450216538705a9e51fa6710/numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e", size = 15150086, upload-time = "2026-06-21T20:56:49.352Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9", size = 16647250, upload-time = "2026-06-21T20:56:51.542Z" }, + { url = "https://files.pythonhosted.org/packages/3c/81/97060281b602ed07f21b12f4ec409eac1f75a2f91fbc829ed8b2becf3ad4/numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab", size = 16512864, upload-time = "2026-06-21T20:56:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/ab/4496208146911f8d8ddb54f68a972aafa6c8d44babcb2ea03b0e5cc87c9d/numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4", size = 18408407, upload-time = "2026-06-21T20:56:57.75Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/a4df67c181e4ee8b467aa3332dc2db10fd5c515136831302f3ca48bc0a01/numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988", size = 6054431, upload-time = "2026-06-21T20:57:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748", size = 12414420, upload-time = "2026-06-21T20:57:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4a/25c2906f541e9d9f4c5769764db732e6627be91a13f4724fa10634d77db4/numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60", size = 10339533, upload-time = "2026-06-21T20:57:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9", size = 16783807, upload-time = "2026-06-21T20:57:06.253Z" }, + { url = "https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446", size = 11765215, upload-time = "2026-06-21T20:57:08.547Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a0/8400a9c0e3625182347593f5e1f57da9a617a534794805c8df5518154ddc/numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c", size = 5324493, upload-time = "2026-06-21T20:57:11.012Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8c/0d104deaa0401c93395a629ec902891618a2eff76d19229139cb5a887bfc/numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303", size = 6645211, upload-time = "2026-06-21T20:57:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d9/4a4a628c812750363786afc3d33492709a5cd64b215469c16b0f6c7bb811/numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22", size = 15166004, upload-time = "2026-06-21T20:57:14.717Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03", size = 16650797, upload-time = "2026-06-21T20:57:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a0/a0090e6329f4ca5992c07847bb579c5259a19953dc57255bb08793142ffb/numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a", size = 16524647, upload-time = "2026-06-21T20:57:19.165Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7d/6caf27734c42b65837e7461ed0dbbd6b6fc835060c9714ec59d673bb383a/numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21", size = 18411841, upload-time = "2026-06-21T20:57:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/26edadbd812536769a82c2e9e002234e33feb5da43061d47a044f6d309b7/numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731", size = 6106361, upload-time = "2026-06-21T20:57:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9e/4dd1459282229a72d92dece2ae9138e5cac94a72263a7ceb48f37434c925/numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73", size = 12551749, upload-time = "2026-06-21T20:57:25.945Z" }, + { url = "https://files.pythonhosted.org/packages/05/a7/6bc6384c080b86c7f6c85c5bc5b540b24f4f679cd144791d99574e90d462/numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e", size = 10617072, upload-time = "2026-06-21T20:57:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/86/6b/4a2b71d66ada5608ae02b63f150dfad520f6940721cb7f029ad270befc0e/numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b", size = 11881067, upload-time = "2026-06-21T20:57:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b2/d365eb40a20efb49d67e9feb90494ed8511282ee1f5fa16006675c65397d/numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927", size = 5440290, upload-time = "2026-06-21T20:57:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/e9c03188de5f9b767e46a8fe988bcfd3efad066a4a3fda8b9cb11a93f895/numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826", size = 6748371, upload-time = "2026-06-21T20:57:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1d/68c186a38a5027bae2c4ddd5ea681fdaf8b4d30fb7301def6d8ad270390f/numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd", size = 15214643, upload-time = "2026-06-21T20:57:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/73f67b7c7e20635baae9c4c3ead4ae7326a005900297a6110971abd62eb5/numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6", size = 16690128, upload-time = "2026-06-21T20:57:38.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/05/d4c1fb0c46d02a27d6b2b8b319a78c90937acec8631c1641874670b31e6f/numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a", size = 16577902, upload-time = "2026-06-21T20:57:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1d/771c797d50fa26e4888989cccf1d50ee51f530d4e455ad2692dcb64fa711/numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9", size = 18452814, upload-time = "2026-06-21T20:57:42.875Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/52fc0d2a68d7643f0f149eeea5a5d8ea2a3507056ac8afa83c9212606e8b/numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7", size = 6253168, upload-time = "2026-06-21T20:57:45.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/6c8d1118b5f13b2881dc095d5b345de19c6638b8959c17409b6eff84c8aa/numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa", size = 12736286, upload-time = "2026-06-21T20:57:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" }, +] + [[package]] name = "openai" version = "2.43.0" @@ -1188,6 +1230,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pgvector" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, +] + [[package]] name = "pip" version = "26.1.2" From 4161fbf3891b4cb1af14dd1bf908e81cb7779531 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:45:03 +0300 Subject: [PATCH 08/81] refactor(models): remove default=None --- .../239f767ca4fd_add_document_chunk_table.py | 32 +++++++++++++++++++ src/ai_notes_api/db/models/chat_memory.py | 1 - src/ai_notes_api/db/models/datetime.py | 1 - 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 alembic/versions/239f767ca4fd_add_document_chunk_table.py diff --git a/alembic/versions/239f767ca4fd_add_document_chunk_table.py b/alembic/versions/239f767ca4fd_add_document_chunk_table.py new file mode 100644 index 0000000..afefaa8 --- /dev/null +++ b/alembic/versions/239f767ca4fd_add_document_chunk_table.py @@ -0,0 +1,32 @@ +"""'Add document_chunk table' + +Revision ID: 239f767ca4fd +Revises: 692926ebb7e2 +Create Date: 2026-06-23 05:44:02.407327 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '239f767ca4fd' +down_revision: Union[str, Sequence[str], None] = '692926ebb7e2' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/src/ai_notes_api/db/models/chat_memory.py b/src/ai_notes_api/db/models/chat_memory.py index 028c070..689c76b 100644 --- a/src/ai_notes_api/db/models/chat_memory.py +++ b/src/ai_notes_api/db/models/chat_memory.py @@ -83,7 +83,6 @@ class ChatMemory(Base, TimestampMixin): ondelete="SET NULL", ), nullable=True, - default=None, index=True, ) diff --git a/src/ai_notes_api/db/models/datetime.py b/src/ai_notes_api/db/models/datetime.py index 6c31ee7..3fe3ad6 100644 --- a/src/ai_notes_api/db/models/datetime.py +++ b/src/ai_notes_api/db/models/datetime.py @@ -42,6 +42,5 @@ class SoftDeleteMixin: deleted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), - default=None, nullable=True, ) From 3221d34b629bfa9f474f8b5b2278053a03d1d8cd Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:45:50 +0300 Subject: [PATCH 09/81] build(alembic): remove default=None --- .../5c4081c49d7e_remove_default_none.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 alembic/versions/5c4081c49d7e_remove_default_none.py diff --git a/alembic/versions/5c4081c49d7e_remove_default_none.py b/alembic/versions/5c4081c49d7e_remove_default_none.py new file mode 100644 index 0000000..c37315f --- /dev/null +++ b/alembic/versions/5c4081c49d7e_remove_default_none.py @@ -0,0 +1,32 @@ +"""'Remove default=None' + +Revision ID: 5c4081c49d7e +Revises: 239f767ca4fd +Create Date: 2026-06-23 05:45:13.909308 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5c4081c49d7e' +down_revision: Union[str, Sequence[str], None] = '239f767ca4fd' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### From ff51c09e2a40d7ecf6cf7b870fd7714d34d2b477 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:52:23 +0300 Subject: [PATCH 10/81] feat(model): add rag_query model --- src/ai_notes_api/db/models/__init__.py | 5 + src/ai_notes_api/db/models/chat_session.py | 10 ++ src/ai_notes_api/db/models/rag_query.py | 156 +++++++++++++++++++++ src/ai_notes_api/db/models/user.py | 7 + 4 files changed, 178 insertions(+) create mode 100644 src/ai_notes_api/db/models/rag_query.py diff --git a/src/ai_notes_api/db/models/__init__.py b/src/ai_notes_api/db/models/__init__.py index 81c906e..b6b2884 100644 --- a/src/ai_notes_api/db/models/__init__.py +++ b/src/ai_notes_api/db/models/__init__.py @@ -8,9 +8,11 @@ from .chat_session import ChatSession, ChatSessionGenerationStatus from .datetime import SoftDeleteMixin, TimestampMixin from .document import Document, DocumentStatus +from .document_chunk import DocumentChunk from .generation_job import GenerationJob, GenerationJobStatus from .message import Message, MessageRole from .note import ModelSource, Note +from .rag_query import RagQuery, RagQueryStatus from .user import User __all__ = [ @@ -29,4 +31,7 @@ "ChatMemory", "Document", "DocumentStatus", + "DocumentChunk", + "RagQuery", + "RagQueryStatus", ] diff --git a/src/ai_notes_api/db/models/chat_session.py b/src/ai_notes_api/db/models/chat_session.py index 0ca0801..0832b1b 100644 --- a/src/ai_notes_api/db/models/chat_session.py +++ b/src/ai_notes_api/db/models/chat_session.py @@ -21,6 +21,7 @@ from ai_notes_api.db.models.document_chunk import DocumentChunk from ai_notes_api.db.models.generation_job import GenerationJob from ai_notes_api.db.models.message import Message + from ai_notes_api.db.models.rag_query import RagQuery from ai_notes_api.db.models.user import User @@ -54,6 +55,10 @@ class ChatSession(Base, TimestampMixin, SoftDeleteMixin): belong to the chat session. memory (Mapped[ChatMemory]): Memory associated with the chat session. documents (Mapped[list[Document]]): Documents that belong to the chat session. + document_chunks (Mapped[list[DocumentChunk]]): Document chunks that + belong to the chat session. + rag_queries (Mapped[list[RagQuery]]): RAG queries that belong to the + chat session. """ __tablename__ = "chat_sessions" @@ -130,3 +135,8 @@ class ChatSession(Base, TimestampMixin, SoftDeleteMixin): back_populates="chat_session", cascade="all, delete-orphan", ) + + rag_queries: Mapped[list["RagQuery"]] = relationship( + back_populates="chat_session", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/rag_query.py b/src/ai_notes_api/db/models/rag_query.py new file mode 100644 index 0000000..93391c2 --- /dev/null +++ b/src/ai_notes_api/db/models/rag_query.py @@ -0,0 +1,156 @@ +"""RAG query database model module. + +This module defines the SQLAlchemy ORM model for RAG queries and the enum used +to track RAG query status. +""" + +from datetime import datetime +from enum import StrEnum +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from sqlalchemy import DateTime, ForeignKey, String, Text, Uuid +from sqlalchemy import Enum as SqlEnum +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from ai_notes_api.db.models.base import Base +from ai_notes_api.db.models.datetime import TimestampMixin + +if TYPE_CHECKING: + from ai_notes_api.db.models.chat_session import ChatSession + from ai_notes_api.db.models.user import User + + +class RagQueryStatus(StrEnum): + """Status of a RAG query. + + Attributes: + QUEUED (str): RAG query is waiting to be processed. + RUNNING (str): RAG query is currently being processed. + COMPLETED (str): RAG query completed successfully. + FAILED (str): RAG query failed. + """ + + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + + +class RagQuery(Base, TimestampMixin): + """SQLAlchemy ORM model representing a RAG query. + + Attributes: + id (Mapped[UUID]): Unique RAG query identifier. + user_id (Mapped[UUID]): Identifier of the user who owns the RAG query. + user (Mapped[User]): User who owns the RAG query. + session_id (Mapped[UUID]): Identifier of the chat session that owns the + RAG query. + chat_session (Mapped[ChatSession]): Chat session that owns the RAG query. + question (Mapped[str]): User question. + answer (Mapped[str | None]): Optional generated answer. + provider (Mapped[str | None]): Optional AI provider name. + model (Mapped[str | None]): Optional AI model name. + prompt_tokens (Mapped[int | None]): Optional number of prompt tokens. + completion_tokens (Mapped[int | None]): Optional number of completion tokens. + total_tokens (Mapped[int | None]): Optional total number of tokens. + top_k (Mapped[int]): Number of document chunks retrieved for the query. + status (Mapped[RagQueryStatus]): Current RAG query status. + finished_at (Mapped[datetime | None]): Date and time when the RAG query + finished. + error_message (Mapped[str | None]): Optional error message if the RAG + query failed. + """ + + __tablename__ = "rag_queries" + + id: Mapped[UUID] = mapped_column( + Uuid, + primary_key=True, + default=uuid4, + ) + + user_id: Mapped[UUID] = mapped_column( + ForeignKey( + "users.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + user: Mapped["User"] = relationship( + back_populates="rag_queries", + ) + + session_id: Mapped[UUID] = mapped_column( + ForeignKey( + "chat_sessions.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + chat_session: Mapped["ChatSession"] = relationship( + back_populates="rag_queries", + ) + + question: Mapped[str] = mapped_column( + Text, + nullable=False, + ) + + answer: Mapped[str | None] = mapped_column( + Text, + default=None, + nullable=True, + ) + + provider: Mapped[str | None] = mapped_column( + String(255), + nullable=True, + ) + + model: Mapped[str | None] = mapped_column( + String(255), + nullable=True, + ) + + prompt_tokens: Mapped[int | None] = mapped_column( + nullable=True, + ) + + completion_tokens: Mapped[int | None] = mapped_column( + nullable=True, + ) + + total_tokens: Mapped[int | None] = mapped_column( + nullable=True, + ) + + top_k: Mapped[int] = mapped_column( + nullable=False, + ) + + status: Mapped[RagQueryStatus] = mapped_column( + SqlEnum( + RagQueryStatus, + name="rag_query_status", + values_callable=lambda enum_cls: [item.value for item in enum_cls], + ), + default=RagQueryStatus.QUEUED, + nullable=False, + ) + + finished_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), + default=None, + nullable=True, + ) + + error_message: Mapped[str | None] = mapped_column( + Text, + default=None, + nullable=True, + ) diff --git a/src/ai_notes_api/db/models/user.py b/src/ai_notes_api/db/models/user.py index cb5576a..9151268 100644 --- a/src/ai_notes_api/db/models/user.py +++ b/src/ai_notes_api/db/models/user.py @@ -18,6 +18,7 @@ from ai_notes_api.db.models.document_chunk import DocumentChunk from ai_notes_api.db.models.generation_job import GenerationJob from ai_notes_api.db.models.note import Note + from ai_notes_api.db.models.rag_query import RagQuery class User(Base, TimestampMixin): @@ -37,6 +38,7 @@ class User(Base, TimestampMixin): documents (Mapped[list[Document]]): Documents owned by the user. document_chunks (Mapped[list[DocumentChunk]]): Document chunks owned by the user. + rag_queries (Mapped[list[RagQuery]]): RAG queries owned by the user. """ __tablename__ = "users" @@ -95,3 +97,8 @@ class User(Base, TimestampMixin): back_populates="user", cascade="all, delete-orphan", ) + + rag_queries: Mapped[list["RagQuery"]] = relationship( + back_populates="user", + cascade="all, delete-orphan", + ) From 834770d497b8fe4a9a006175c41256f4a7d92c4d Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:53:01 +0300 Subject: [PATCH 11/81] build(alembic): add rag_queries table --- .../617d7be6fd51_add_rag_queries_table.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 alembic/versions/617d7be6fd51_add_rag_queries_table.py diff --git a/alembic/versions/617d7be6fd51_add_rag_queries_table.py b/alembic/versions/617d7be6fd51_add_rag_queries_table.py new file mode 100644 index 0000000..ae4f6c2 --- /dev/null +++ b/alembic/versions/617d7be6fd51_add_rag_queries_table.py @@ -0,0 +1,82 @@ +"""'Add rag_queries table' + +Revision ID: 617d7be6fd51 +Revises: 5c4081c49d7e +Create Date: 2026-06-23 05:52:35.802978 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '617d7be6fd51' +down_revision: Union[str, Sequence[str], None] = '5c4081c49d7e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('rag_queries', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('session_id', sa.Uuid(), nullable=False), + sa.Column('question', sa.Text(), nullable=False), + sa.Column('answer', sa.Text(), nullable=True), + sa.Column('provider', sa.String(length=255), nullable=True), + sa.Column('model', sa.String(length=255), nullable=True), + sa.Column('prompt_tokens', sa.Integer(), nullable=True), + sa.Column('completion_tokens', sa.Integer(), nullable=True), + sa.Column('total_tokens', sa.Integer(), nullable=True), + sa.Column('top_k', sa.Integer(), nullable=False), + sa.Column('status', sa.Enum('queued', 'running', 'completed', 'failed', name='rag_query_status'), nullable=False), + sa.Column('finished_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['session_id'], ['chat_sessions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rag_queries_session_id'), 'rag_queries', ['session_id'], unique=False) + op.create_index(op.f('ix_rag_queries_user_id'), 'rag_queries', ['user_id'], unique=False) + op.create_table('document_chunks', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('session_id', sa.Uuid(), nullable=False), + sa.Column('document_id', sa.Uuid(), nullable=False), + sa.Column('chunk_index', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('content_hash', sa.String(length=255), nullable=False), + sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=1536), nullable=False), + sa.Column('embedding_model', sa.String(length=255), nullable=False), + sa.Column('token_count', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['document_id'], ['documents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['session_id'], ['chat_sessions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_document_chunks_document_id'), 'document_chunks', ['document_id'], unique=False) + op.create_index(op.f('ix_document_chunks_session_id'), 'document_chunks', ['session_id'], unique=False) + op.create_index(op.f('ix_document_chunks_user_id'), 'document_chunks', ['user_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_document_chunks_user_id'), table_name='document_chunks') + op.drop_index(op.f('ix_document_chunks_session_id'), table_name='document_chunks') + op.drop_index(op.f('ix_document_chunks_document_id'), table_name='document_chunks') + op.drop_table('document_chunks') + op.drop_index(op.f('ix_rag_queries_user_id'), table_name='rag_queries') + op.drop_index(op.f('ix_rag_queries_session_id'), table_name='rag_queries') + op.drop_table('rag_queries') + # ### end Alembic commands ### From 52734ef29c329b0069c5d691f227799d934df0cc Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:55:55 +0300 Subject: [PATCH 12/81] feat(model): add rag_query_source model --- src/ai_notes_api/db/models/__init__.py | 2 + src/ai_notes_api/db/models/document.py | 8 ++ src/ai_notes_api/db/models/document_chunk.py | 8 ++ src/ai_notes_api/db/models/rag_query.py | 8 ++ .../db/models/rag_query_source.py | 98 +++++++++++++++++++ 5 files changed, 124 insertions(+) create mode 100644 src/ai_notes_api/db/models/rag_query_source.py diff --git a/src/ai_notes_api/db/models/__init__.py b/src/ai_notes_api/db/models/__init__.py index b6b2884..df2fccd 100644 --- a/src/ai_notes_api/db/models/__init__.py +++ b/src/ai_notes_api/db/models/__init__.py @@ -13,6 +13,7 @@ from .message import Message, MessageRole from .note import ModelSource, Note from .rag_query import RagQuery, RagQueryStatus +from .rag_query_source import RagQuerySource from .user import User __all__ = [ @@ -34,4 +35,5 @@ "DocumentChunk", "RagQuery", "RagQueryStatus", + "RagQuerySource", ] diff --git a/src/ai_notes_api/db/models/document.py b/src/ai_notes_api/db/models/document.py index e289fd6..7baefd6 100644 --- a/src/ai_notes_api/db/models/document.py +++ b/src/ai_notes_api/db/models/document.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession from ai_notes_api.db.models.document_chunk import DocumentChunk + from ai_notes_api.db.models.rag_query_source import RagQuerySource from ai_notes_api.db.models.user import User @@ -61,6 +62,8 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): processing failed. document_chunks (Mapped[list[DocumentChunk]]): Chunks that belong to the document. + rag_query_sources (Mapped[list[RagQuerySource]]): RAG query sources that + reference the document. """ __tablename__ = "documents" @@ -144,3 +147,8 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): back_populates="document", cascade="all, delete-orphan", ) + + rag_query_sources: Mapped[list["RagQuerySource"]] = relationship( + back_populates="document", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/document_chunk.py b/src/ai_notes_api/db/models/document_chunk.py index 663cae3..b73d111 100644 --- a/src/ai_notes_api/db/models/document_chunk.py +++ b/src/ai_notes_api/db/models/document_chunk.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession from ai_notes_api.db.models.document import Document + from ai_notes_api.db.models.rag_query_source import RagQuerySource from ai_notes_api.db.models.user import User @@ -43,6 +44,8 @@ class DocumentChunk(Base, TimestampMixin, SoftDeleteMixin): embedding. token_count (Mapped[int | None]): Optional number of tokens in the chunk. + rag_query_sources (Mapped[list[RagQuerySource]]): RAG query sources that + reference the chunk. """ __tablename__ = "document_chunks" @@ -119,3 +122,8 @@ class DocumentChunk(Base, TimestampMixin, SoftDeleteMixin): token_count: Mapped[int | None] = mapped_column( nullable=True, ) + + rag_query_sources: Mapped[list["RagQuerySource"]] = relationship( + back_populates="chunk", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/rag_query.py b/src/ai_notes_api/db/models/rag_query.py index 93391c2..18580c7 100644 --- a/src/ai_notes_api/db/models/rag_query.py +++ b/src/ai_notes_api/db/models/rag_query.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession + from ai_notes_api.db.models.rag_query_source import RagQuerySource from ai_notes_api.db.models.user import User @@ -60,6 +61,8 @@ class RagQuery(Base, TimestampMixin): finished. error_message (Mapped[str | None]): Optional error message if the RAG query failed. + sources (Mapped[list[RagQuerySource]]): Sources retrieved for the RAG + query. """ __tablename__ = "rag_queries" @@ -154,3 +157,8 @@ class RagQuery(Base, TimestampMixin): default=None, nullable=True, ) + + sources: Mapped[list["RagQuerySource"]] = relationship( + back_populates="rag_query", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/rag_query_source.py b/src/ai_notes_api/db/models/rag_query_source.py new file mode 100644 index 0000000..5026951 --- /dev/null +++ b/src/ai_notes_api/db/models/rag_query_source.py @@ -0,0 +1,98 @@ +"""RAG query source database model module. + +This module defines the SQLAlchemy ORM model for RAG query sources, which link a +RAG query to the document chunks retrieved for it. +""" + +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from sqlalchemy import Float, ForeignKey, Text, Uuid +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from ai_notes_api.db.models.base import Base +from ai_notes_api.db.models.datetime import TimestampMixin + +if TYPE_CHECKING: + from ai_notes_api.db.models.document import Document + from ai_notes_api.db.models.document_chunk import DocumentChunk + from ai_notes_api.db.models.rag_query import RagQuery + + +class RagQuerySource(Base, TimestampMixin): + """SQLAlchemy ORM model representing a RAG query source. + + Attributes: + id (Mapped[UUID]): Unique RAG query source identifier. + rag_query_id (Mapped[UUID]): Identifier of the RAG query the source + belongs to. + rag_query (Mapped[RagQuery]): RAG query the source belongs to. + document_id (Mapped[UUID]): Identifier of the source document. + document (Mapped[Document]): Source document. + chunk_id (Mapped[UUID]): Identifier of the source document chunk. + chunk (Mapped[DocumentChunk]): Source document chunk. + score (Mapped[float]): Relevance score of the chunk for the query. + rank (Mapped[int]): Rank of the chunk among the retrieved sources. + content_preview (Mapped[str]): Preview of the chunk content. + """ + + __tablename__ = "rag_query_sources" + + id: Mapped[UUID] = mapped_column( + Uuid, + primary_key=True, + default=uuid4, + ) + + rag_query_id: Mapped[UUID] = mapped_column( + ForeignKey( + "rag_queries.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + rag_query: Mapped["RagQuery"] = relationship( + back_populates="sources", + ) + + document_id: Mapped[UUID] = mapped_column( + ForeignKey( + "documents.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + document: Mapped["Document"] = relationship( + back_populates="rag_query_sources", + ) + + chunk_id: Mapped[UUID] = mapped_column( + ForeignKey( + "document_chunks.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + chunk: Mapped["DocumentChunk"] = relationship( + back_populates="rag_query_sources", + ) + + score: Mapped[float] = mapped_column( + Float, + nullable=False, + ) + + rank: Mapped[int] = mapped_column( + nullable=False, + ) + + content_preview: Mapped[str] = mapped_column( + Text, + nullable=False, + ) From 2f834616f07ef759b12d506654b7f0f19dc7fec5 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:57:28 +0300 Subject: [PATCH 13/81] build(alembic): fix rag_queries table vector type --- alembic/versions/617d7be6fd51_add_rag_queries_table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/alembic/versions/617d7be6fd51_add_rag_queries_table.py b/alembic/versions/617d7be6fd51_add_rag_queries_table.py index ae4f6c2..588d216 100644 --- a/alembic/versions/617d7be6fd51_add_rag_queries_table.py +++ b/alembic/versions/617d7be6fd51_add_rag_queries_table.py @@ -8,6 +8,7 @@ from typing import Sequence, Union from alembic import op +from pgvector.sqlalchemy import VECTOR import sqlalchemy as sa @@ -52,7 +53,7 @@ def upgrade() -> None: sa.Column('chunk_index', sa.Integer(), nullable=False), sa.Column('content', sa.Text(), nullable=False), sa.Column('content_hash', sa.String(length=255), nullable=False), - sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=1536), nullable=False), + sa.Column('embedding', VECTOR(dim=1536), nullable=False), sa.Column('embedding_model', sa.String(length=255), nullable=False), sa.Column('token_count', sa.Integer(), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), From 8b2f031d66ed2806a04afbf8f752e09377816473 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 05:58:05 +0300 Subject: [PATCH 14/81] build(alembic): add rag_query_sources table --- ...495a670b858_add_rag_query_sources_table.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 alembic/versions/e495a670b858_add_rag_query_sources_table.py diff --git a/alembic/versions/e495a670b858_add_rag_query_sources_table.py b/alembic/versions/e495a670b858_add_rag_query_sources_table.py new file mode 100644 index 0000000..0f4e74a --- /dev/null +++ b/alembic/versions/e495a670b858_add_rag_query_sources_table.py @@ -0,0 +1,52 @@ +"""'Add rag_query_sources table' + +Revision ID: e495a670b858 +Revises: 617d7be6fd51 +Create Date: 2026-06-23 05:57:34.226842 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e495a670b858' +down_revision: Union[str, Sequence[str], None] = '617d7be6fd51' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('rag_query_sources', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('rag_query_id', sa.Uuid(), nullable=False), + sa.Column('document_id', sa.Uuid(), nullable=False), + sa.Column('chunk_id', sa.Uuid(), nullable=False), + sa.Column('score', sa.Float(), nullable=False), + sa.Column('rank', sa.Integer(), nullable=False), + sa.Column('content_preview', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['chunk_id'], ['document_chunks.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['document_id'], ['documents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['rag_query_id'], ['rag_queries.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rag_query_sources_chunk_id'), 'rag_query_sources', ['chunk_id'], unique=False) + op.create_index(op.f('ix_rag_query_sources_document_id'), 'rag_query_sources', ['document_id'], unique=False) + op.create_index(op.f('ix_rag_query_sources_rag_query_id'), 'rag_query_sources', ['rag_query_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_rag_query_sources_rag_query_id'), table_name='rag_query_sources') + op.drop_index(op.f('ix_rag_query_sources_document_id'), table_name='rag_query_sources') + op.drop_index(op.f('ix_rag_query_sources_chunk_id'), table_name='rag_query_sources') + op.drop_table('rag_query_sources') + # ### end Alembic commands ### From ab41aad0adc280f0b212785781b34b8be31bd7f7 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:02:45 +0300 Subject: [PATCH 15/81] style(model): fix RagQuery docstring --- src/ai_notes_api/db/models/rag_query.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ai_notes_api/db/models/rag_query.py b/src/ai_notes_api/db/models/rag_query.py index 18580c7..5fd1b81 100644 --- a/src/ai_notes_api/db/models/rag_query.py +++ b/src/ai_notes_api/db/models/rag_query.py @@ -61,8 +61,7 @@ class RagQuery(Base, TimestampMixin): finished. error_message (Mapped[str | None]): Optional error message if the RAG query failed. - sources (Mapped[list[RagQuerySource]]): Sources retrieved for the RAG - query. + sources (Mapped[list[RagQuerySource]]): Sources retrieved for the RAG query. """ __tablename__ = "rag_queries" From c66664a9d261de8c85ec3429b2fae6134feaf353 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:03:02 +0300 Subject: [PATCH 16/81] docs(readme): add erd --- README.md | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/README.md b/README.md index 214227d..6316143 100755 --- a/README.md +++ b/README.md @@ -165,6 +165,176 @@ task lint task check ``` +## 🗄 Database schema + +Entity-relationship diagram for the database models: + +```mermaid +erDiagram + users { + uuid id PK + string email UK + string username "null" + string hashed_password + bool is_active + bool is_superuser + datetime created_at + datetime updated_at + } + + notes { + uuid id PK + uuid user_id FK + string title + text content + string_array tags + enum source "model_source" + string model_name "null" + jsonb model_metadata + datetime created_at + datetime updated_at + datetime deleted_at "null" + } + + chat_sessions { + uuid id PK + uuid user_id FK + string title + enum generation_status "chat_session_generation_status" + uuid generation_id "null" + datetime generation_started_at "null" + datetime created_at + datetime updated_at + datetime deleted_at "null" + } + + messages { + uuid id PK + uuid session_id FK + text content + enum role "message_role" + string provider "null" + string model_name "null" + int prompt_tokens "null" + int completion_tokens "null" + int total_tokens "null" + datetime created_at + datetime updated_at + datetime deleted_at "null" + } + + chat_memories { + uuid id PK + uuid session_id FK,UK + text summary + jsonb facts + bool is_summarizing + uuid last_summarized_message_id FK "null" + datetime created_at + datetime updated_at + } + + generation_jobs { + uuid id PK + uuid user_id FK + uuid session_id FK + enum status "generation_job_status" + text input_message + uuid output_message_id FK "null" + text error "null" + datetime started_at "null" + datetime finished_at "null" + datetime created_at + datetime updated_at + } + + documents { + uuid id PK + uuid user_id FK + uuid session_id FK + string filename + string content_type + int file_size + string checksum_sha256 + string storage_bucket + string storage_object_name + enum status "document_status" + text error_message "null" + datetime created_at + datetime updated_at + datetime deleted_at "null" + } + + document_chunks { + uuid id PK + uuid user_id FK + uuid session_id FK + uuid document_id FK + int chunk_index + text content + string content_hash + vector embedding + string embedding_model + int token_count "null" + datetime created_at + datetime updated_at + datetime deleted_at "null" + } + + rag_queries { + uuid id PK + uuid user_id FK + uuid session_id FK + text question + text answer "null" + string provider "null" + string model "null" + int prompt_tokens "null" + int completion_tokens "null" + int total_tokens "null" + int top_k + enum status "rag_query_status" + datetime finished_at "null" + text error_message "null" + datetime created_at + datetime updated_at + } + + rag_query_sources { + uuid id PK + uuid rag_query_id FK + uuid document_id FK + uuid chunk_id FK + float score + int rank + text content_preview + datetime created_at + datetime updated_at + } + + users ||--o{ notes : owns + users ||--o{ chat_sessions : owns + users ||--o{ generation_jobs : owns + users ||--o{ documents : owns + users ||--o{ document_chunks : owns + users ||--o{ rag_queries : owns + + chat_sessions ||--o{ messages : contains + chat_sessions ||--o| chat_memories : has + chat_sessions ||--o{ generation_jobs : contains + chat_sessions ||--o{ documents : contains + chat_sessions ||--o{ document_chunks : contains + chat_sessions ||--o{ rag_queries : contains + + messages ||--o| generation_jobs : "output of" + messages ||--o| chat_memories : "last summarized" + + documents ||--o{ document_chunks : "split into" + documents ||--o{ rag_query_sources : "referenced by" + document_chunks ||--o{ rag_query_sources : "referenced by" + rag_queries ||--o{ rag_query_sources : "retrieved" +``` + ## 🛠 Database migrations * Create a new Alembic revision: From 86f896cfb41f23d2a7f5e47ef03d46e7fccde0d6 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:13:34 +0300 Subject: [PATCH 17/81] feat(repository): add document repository --- src/ai_notes_api/db/models/document_chunk.py | 15 +- src/ai_notes_api/repositories/document.py | 169 +++++++++++++++++++ 2 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 src/ai_notes_api/repositories/document.py diff --git a/src/ai_notes_api/db/models/document_chunk.py b/src/ai_notes_api/db/models/document_chunk.py index b73d111..dd6ce8e 100644 --- a/src/ai_notes_api/db/models/document_chunk.py +++ b/src/ai_notes_api/db/models/document_chunk.py @@ -26,24 +26,19 @@ class DocumentChunk(Base, TimestampMixin, SoftDeleteMixin): Attributes: id (Mapped[UUID]): Unique document chunk identifier. - user_id (Mapped[UUID]): Identifier of the user who owns the document - chunk. + user_id (Mapped[UUID]): Identifier of the user who owns the document chunk. user (Mapped[User]): User who owns the document chunk. session_id (Mapped[UUID]): Identifier of the chat session that owns the document chunk. - chat_session (Mapped[ChatSession]): Chat session that owns the document - chunk. - document_id (Mapped[UUID]): Identifier of the document the chunk belongs - to. + chat_session (Mapped[ChatSession]): Chat session that owns the document chunk. + document_id (Mapped[UUID]): Identifier of the document the chunk belongs to. document (Mapped[Document]): Document the chunk belongs to. chunk_index (Mapped[int]): Position of the chunk within the document. content (Mapped[str]): Text content of the chunk. content_hash (Mapped[str]): Hash of the chunk content. embedding (Mapped[list[float]]): Vector embedding of the chunk content. - embedding_model (Mapped[str]): Name of the model used to produce the - embedding. - token_count (Mapped[int | None]): Optional number of tokens in the - chunk. + embedding_model (Mapped[str]): Name of the model used to produce the embedding. + token_count (Mapped[int | None]): Optional number of tokens in the chunk. rag_query_sources (Mapped[list[RagQuerySource]]): RAG query sources that reference the chunk. """ diff --git a/src/ai_notes_api/repositories/document.py b/src/ai_notes_api/repositories/document.py new file mode 100644 index 0000000..4b80ca8 --- /dev/null +++ b/src/ai_notes_api/repositories/document.py @@ -0,0 +1,169 @@ +"""Document repository module. + +This module provides a repository for creating, reading, updating, and +soft-deleting documents in the database. +""" + +from datetime import UTC, datetime +from uuid import UUID + +from loguru import logger +from sqlalchemy import select, update + +from ai_notes_api.db.models import Document, DocumentChunk +from ai_notes_api.repositories.base import BaseRepository + + +class DocumentRepository(BaseRepository): + """Repository for document database operations.""" + + async def create(self, document: Document) -> Document: + """Create a document in the database. + + Args: + document (Document): Document instance to persist. + + Returns: + Document: Persisted document with refreshed database-generated fields. + """ + self.session.add(document) + + await self.session.flush() + await self.session.refresh(document) + + logger.info("Document created: id={}", document.id) + + return document + + async def get_by_id(self, document_id: UUID) -> Document | None: + """Return a document by its identifier. + + Args: + document_id (UUID): Unique document identifier. + + Returns: + Document | None: Matching document if found and not soft-deleted; + otherwise, None. + """ + stmt = ( + select(Document) + .where(Document.id == document_id) + .where(Document.deleted_at.is_(None)) + ) + + result = await self.session.execute(stmt) + document = result.scalar_one_or_none() + + if document is None: + logger.debug("Document not found: id={}", document_id) + else: + logger.debug("Document found: id={}", document_id) + + return document + + async def get_by_id_for_user( + self, + user_id: UUID, + document_id: UUID, + ) -> Document | None: + """Return a user's document by its identifier. + + Args: + user_id (UUID): Unique identifier of the user who owns the document. + document_id (UUID): Unique document identifier. + + Returns: + Document | None: Matching document if found and not soft-deleted; + otherwise, None. + """ + stmt = ( + select(Document) + .where(Document.user_id == user_id) + .where(Document.id == document_id) + .where(Document.deleted_at.is_(None)) + ) + + result = await self.session.execute(stmt) + document = result.scalar_one_or_none() + + if document is None: + logger.debug("Document not found: id={}", document_id) + else: + logger.debug("Document found: id={}", document_id) + + return document + + async def get_list_for_session( + self, + user_id: UUID, + session_id: UUID, + ) -> list[Document]: + """Return a user's documents for a chat session. + + Args: + user_id (UUID): Unique identifier of the user who owns the documents. + session_id (UUID): Unique chat session identifier. + + Returns: + list[Document]: List of matching non-deleted documents ordered by + creation date in descending order. + """ + stmt = ( + select(Document) + .where(Document.user_id == user_id) + .where(Document.session_id == session_id) + .where(Document.deleted_at.is_(None)) + .order_by(Document.created_at.desc()) + ) + + result = await self.session.execute(stmt) + documents = list(result.scalars().all()) + + logger.debug( + "Documents list fetched: count={}, user_id={}, session_id={}", + len(documents), + user_id, + session_id, + ) + + return documents + + async def update(self, document: Document) -> Document: + """Update an existing document in the database. + + Args: + document (Document): Document instance with updated field values. + + Returns: + Document: Updated and refreshed document instance. + """ + await self.session.flush() + await self.session.refresh(document) + + logger.info("Document updated: id={}", document.id) + + return document + + async def soft_delete(self, document: Document) -> None: + """Soft-delete a document. + + Sets the deletion timestamp for the given document instead of removing + the row from the database. + + Args: + document (Document): Document instance to soft-delete. + """ + now = datetime.now(UTC) + + document.deleted_at = now + + await self.session.flush() + + await self.session.execute( + update(DocumentChunk) + .where(DocumentChunk.document_id == document.id) + .where(DocumentChunk.deleted_at.is_(None)) + .values(deleted_at=now) + ) + + logger.info("Document soft-deleted: id={}", document.id) From 1253bf368addbefb99202bac9fb56543939d45ca Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:15:50 +0300 Subject: [PATCH 18/81] feat(repostitory): add rag_query repository --- src/ai_notes_api/repositories/rag_query.py | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/ai_notes_api/repositories/rag_query.py diff --git a/src/ai_notes_api/repositories/rag_query.py b/src/ai_notes_api/repositories/rag_query.py new file mode 100644 index 0000000..b4a1cb6 --- /dev/null +++ b/src/ai_notes_api/repositories/rag_query.py @@ -0,0 +1,137 @@ +"""RAG query repository module. + +This module provides a repository for creating, reading, and updating RAG +queries in the database. +""" + +from uuid import UUID + +from loguru import logger +from sqlalchemy import select + +from ai_notes_api.db.models import RagQuery +from ai_notes_api.repositories.base import BaseRepository + + +class RagQueryRepository(BaseRepository): + """Repository for RAG query database operations.""" + + async def create(self, rag_query: RagQuery) -> RagQuery: + """Create a RAG query in the database. + + Args: + rag_query (RagQuery): RAG query instance to persist. + + Returns: + RagQuery: Persisted RAG query with refreshed database-generated + fields. + """ + self.session.add(rag_query) + + await self.session.flush() + await self.session.refresh(rag_query) + + logger.info("RAG query created: id={}", rag_query.id) + + return rag_query + + async def get_by_id(self, query_id: UUID) -> RagQuery | None: + """Return a RAG query by its identifier. + + Args: + query_id (UUID): Unique RAG query identifier. + + Returns: + RagQuery | None: Matching RAG query if found; otherwise, None. + """ + stmt = select(RagQuery).where(RagQuery.id == query_id) + + result = await self.session.execute(stmt) + rag_query = result.scalar_one_or_none() + + if rag_query is None: + logger.debug("RAG query not found: id={}", query_id) + else: + logger.debug("RAG query found: id={}", query_id) + + return rag_query + + async def get_by_id_for_user( + self, + user_id: UUID, + query_id: UUID, + ) -> RagQuery | None: + """Return a user's RAG query by its identifier. + + Args: + user_id (UUID): Unique identifier of the user who owns the RAG query. + query_id (UUID): Unique RAG query identifier. + + Returns: + RagQuery | None: Matching RAG query if found; otherwise, None. + """ + stmt = ( + select(RagQuery) + .where(RagQuery.user_id == user_id) + .where(RagQuery.id == query_id) + ) + + result = await self.session.execute(stmt) + rag_query = result.scalar_one_or_none() + + if rag_query is None: + logger.debug("RAG query not found: id={}", query_id) + else: + logger.debug("RAG query found: id={}", query_id) + + return rag_query + + async def get_list_for_session( + self, + user_id: UUID, + session_id: UUID, + ) -> list[RagQuery]: + """Return a user's RAG queries for a chat session. + + Args: + user_id (UUID): Unique identifier of the user who owns the RAG queries. + session_id (UUID): Unique chat session identifier. + + Returns: + list[RagQuery]: List of matching RAG queries ordered by creation date + in descending order. + """ + stmt = ( + select(RagQuery) + .where(RagQuery.user_id == user_id) + .where(RagQuery.session_id == session_id) + .order_by(RagQuery.created_at.desc()) + ) + + result = await self.session.execute(stmt) + rag_queries = list(result.scalars().all()) + + logger.debug( + "RAG queries list fetched: count={}, user_id={}, session_id={}", + len(rag_queries), + user_id, + session_id, + ) + + return rag_queries + + async def update(self, rag_query: RagQuery) -> RagQuery: + """Update an existing RAG query in the database. + + Args: + rag_query (RagQuery): RAG query instance with updated field values. + + Returns: + RagQuery: Updated and refreshed RAG query instance. + """ + await self.session.flush() + await self.session.refresh(rag_query) + + logger.info("RAG query updated: id={}", rag_query.id) + + return rag_query From 1657ce1e4387560b498050bcf1e539682560512e Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:16:16 +0300 Subject: [PATCH 19/81] feat(repository): add rag_query_source repository --- .../repositories/rag_query_source.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/ai_notes_api/repositories/rag_query_source.py diff --git a/src/ai_notes_api/repositories/rag_query_source.py b/src/ai_notes_api/repositories/rag_query_source.py new file mode 100644 index 0000000..2d10f84 --- /dev/null +++ b/src/ai_notes_api/repositories/rag_query_source.py @@ -0,0 +1,95 @@ +"""RAG query source repository module. + +This module provides a repository for creating and reading RAG query sources in +the database. +""" + +from collections.abc import Sequence +from uuid import UUID + +from loguru import logger +from sqlalchemy import select + +from ai_notes_api.db.models import RagQuerySource +from ai_notes_api.repositories.base import BaseRepository + + +class RagQuerySourceRepository(BaseRepository): + """Repository for RAG query source database operations.""" + + async def create(self, rag_query_source: RagQuerySource) -> RagQuerySource: + """Create a RAG query source in the database. + + Args: + rag_query_source (RagQuerySource): RAG query source instance to + persist. + + Returns: + RagQuerySource: Persisted RAG query source with refreshed + database-generated fields. + """ + self.session.add(rag_query_source) + + await self.session.flush() + await self.session.refresh(rag_query_source) + + logger.info("RAG query source created: id={}", rag_query_source.id) + + return rag_query_source + + async def create_many( + self, + rag_query_sources: Sequence[RagQuerySource], + ) -> list[RagQuerySource]: + """Create multiple RAG query sources in the database. + + Args: + rag_query_sources (Sequence[RagQuerySource]): RAG query source + instances to persist. + + Returns: + list[RagQuerySource]: Persisted RAG query sources with refreshed + database-generated fields. + """ + rag_query_sources = list(rag_query_sources) + + self.session.add_all(rag_query_sources) + + await self.session.flush() + + for rag_query_source in rag_query_sources: + await self.session.refresh(rag_query_source) + + logger.info("RAG query sources created: count={}", len(rag_query_sources)) + + return rag_query_sources + + async def get_list_for_rag_query( + self, + rag_query_id: UUID, + ) -> list[RagQuerySource]: + """Return RAG query sources for a RAG query. + + Args: + rag_query_id (UUID): Unique RAG query identifier. + + Returns: + list[RagQuerySource]: List of matching RAG query sources ordered by + rank in ascending order. + """ + stmt = ( + select(RagQuerySource) + .where(RagQuerySource.rag_query_id == rag_query_id) + .order_by(RagQuerySource.rank.asc()) + ) + + result = await self.session.execute(stmt) + rag_query_sources = list(result.scalars().all()) + + logger.debug( + "RAG query sources list fetched: count={}, rag_query_id={}", + len(rag_query_sources), + rag_query_id, + ) + + return rag_query_sources From 4c8273357237fd4afd6a92b76255f9bb98f80d45 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:25:28 +0300 Subject: [PATCH 20/81] feat(model): add processed_at in document model --- src/ai_notes_api/db/models/document.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ai_notes_api/db/models/document.py b/src/ai_notes_api/db/models/document.py index 7baefd6..1c5d8eb 100644 --- a/src/ai_notes_api/db/models/document.py +++ b/src/ai_notes_api/db/models/document.py @@ -4,12 +4,13 @@ used to track document processing status. """ +from datetime import datetime from enum import StrEnum from typing import TYPE_CHECKING from uuid import UUID, uuid4 +from sqlalchemy import DateTime, ForeignKey, String, Text, Uuid from sqlalchemy import Enum as SqlEnum -from sqlalchemy import ForeignKey, String, Text, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship from ai_notes_api.db.models.base import Base @@ -60,6 +61,8 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): status (Mapped[DocumentStatus]): Current document processing status. error_message (Mapped[str | None]): Optional error message if document processing failed. + processed_at (Mapped[datetime | None]): Date and time when the document + finished processing. document_chunks (Mapped[list[DocumentChunk]]): Chunks that belong to the document. rag_query_sources (Mapped[list[RagQuerySource]]): RAG query sources that @@ -143,6 +146,12 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): nullable=True, ) + processed_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), + default=None, + nullable=True, + ) + document_chunks: Mapped[list["DocumentChunk"]] = relationship( back_populates="document", cascade="all, delete-orphan", From 30fcfb2b6fa4a330871d5ee55599b5bf4feb9110 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:26:15 +0300 Subject: [PATCH 21/81] build(alembic): add processed_at column in documents --- ...e8_add_processed_at_column_in_documents.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py diff --git a/alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py b/alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py new file mode 100644 index 0000000..c37bf08 --- /dev/null +++ b/alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py @@ -0,0 +1,32 @@ +"""'Add processed_at column in documents' + +Revision ID: 89a8d53bd0e8 +Revises: e495a670b858 +Create Date: 2026-06-23 06:25:52.857746 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '89a8d53bd0e8' +down_revision: Union[str, Sequence[str], None] = 'e495a670b858' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('documents', sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('documents', 'processed_at') + # ### end Alembic commands ### From ab3ed6319e4521222cb6fb53aa81d33905313660 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:26:32 +0300 Subject: [PATCH 22/81] docs(readme): fix erd --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6316143..8b141e3 100755 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ erDiagram string storage_object_name enum status "document_status" text error_message "null" + datetime processed_at "null" datetime created_at datetime updated_at datetime deleted_at "null" From eb8a50bb94f7ce63493364f76571438eab75c0c1 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:27:12 +0300 Subject: [PATCH 23/81] docs(repository): update docstring in soft_delete func --- src/ai_notes_api/repositories/document.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ai_notes_api/repositories/document.py b/src/ai_notes_api/repositories/document.py index 4b80ca8..48391bb 100644 --- a/src/ai_notes_api/repositories/document.py +++ b/src/ai_notes_api/repositories/document.py @@ -145,10 +145,10 @@ async def update(self, document: Document) -> Document: return document async def soft_delete(self, document: Document) -> None: - """Soft-delete a document. + """Soft-delete a document and its chunks. - Sets the deletion timestamp for the given document instead of removing - the row from the database. + Sets the deletion timestamp for the given document and all of its + non-deleted chunks instead of removing rows from the database. Args: document (Document): Document instance to soft-delete. From aae56701cf9a41ef9002f614f50d44f47b92e277 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:27:37 +0300 Subject: [PATCH 24/81] feat(repository): add document_chunk repository --- .../repositories/document_chunk.py | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/ai_notes_api/repositories/document_chunk.py diff --git a/src/ai_notes_api/repositories/document_chunk.py b/src/ai_notes_api/repositories/document_chunk.py new file mode 100644 index 0000000..d087800 --- /dev/null +++ b/src/ai_notes_api/repositories/document_chunk.py @@ -0,0 +1,201 @@ +"""Document chunk repository module. + +This module provides a repository for creating, reading, updating, and +soft-deleting document chunks in the database. +""" + +from collections.abc import Sequence +from datetime import UTC, datetime +from uuid import UUID + +from loguru import logger +from sqlalchemy import select, update + +from ai_notes_api.db.models import DocumentChunk +from ai_notes_api.repositories.base import BaseRepository + + +class DocumentChunkRepository(BaseRepository): + """Repository for document chunk database operations.""" + + async def create(self, document_chunk: DocumentChunk) -> DocumentChunk: + """Create a document chunk in the database. + + Args: + document_chunk (DocumentChunk): Document chunk instance to persist. + + Returns: + DocumentChunk: Persisted document chunk with refreshed + database-generated fields. + """ + self.session.add(document_chunk) + + await self.session.flush() + await self.session.refresh(document_chunk) + + logger.info("Document chunk created: id={}", document_chunk.id) + + return document_chunk + + async def create_many( + self, + document_chunks: Sequence[DocumentChunk], + ) -> list[DocumentChunk]: + """Create multiple document chunks in the database. + + Args: + document_chunks (Sequence[DocumentChunk]): Document chunk instances to + persist. + + Returns: + list[DocumentChunk]: Persisted document chunks with refreshed + database-generated fields. + """ + document_chunks = list(document_chunks) + + self.session.add_all(document_chunks) + + await self.session.flush() + + for document_chunk in document_chunks: + await self.session.refresh(document_chunk) + + logger.info("Document chunks created: count={}", len(document_chunks)) + + return document_chunks + + async def get_by_id(self, chunk_id: UUID) -> DocumentChunk | None: + """Return a document chunk by its identifier. + + Args: + chunk_id (UUID): Unique document chunk identifier. + + Returns: + DocumentChunk | None: Matching document chunk if found and not + soft-deleted; otherwise, None. + """ + stmt = ( + select(DocumentChunk) + .where(DocumentChunk.id == chunk_id) + .where(DocumentChunk.deleted_at.is_(None)) + ) + + result = await self.session.execute(stmt) + document_chunk = result.scalar_one_or_none() + + if document_chunk is None: + logger.debug("Document chunk not found: id={}", chunk_id) + else: + logger.debug("Document chunk found: id={}", chunk_id) + + return document_chunk + + async def get_list_for_document(self, document_id: UUID) -> list[DocumentChunk]: + """Return document chunks for a document. + + Args: + document_id (UUID): Unique document identifier. + + Returns: + list[DocumentChunk]: List of matching non-deleted document chunks + ordered by chunk index in ascending order. + """ + stmt = ( + select(DocumentChunk) + .where(DocumentChunk.document_id == document_id) + .where(DocumentChunk.deleted_at.is_(None)) + .order_by(DocumentChunk.chunk_index.asc()) + ) + + result = await self.session.execute(stmt) + document_chunks = list(result.scalars().all()) + + logger.debug( + "Document chunks list fetched: count={}, document_id={}", + len(document_chunks), + document_id, + ) + + return document_chunks + + async def search_in_user_session( + self, + query_embedding: list[float], + user_id: UUID, + session_id: UUID, + limit: int = 5, + ) -> list[DocumentChunk]: + """Return the most similar document chunks in a user's chat session. + + Args: + query_embedding (list[float]): Query vector embedding to compare + chunk embeddings against. + user_id (UUID): Unique identifier of the user who owns the chunks. + session_id (UUID): Unique chat session identifier. + limit (int): Maximum number of chunks to return. + + Returns: + list[DocumentChunk]: List of matching non-deleted document chunks + ordered by cosine distance to the query embedding in ascending order. + """ + distance = DocumentChunk.embedding.cosine_distance(query_embedding) + + stmt = ( + select(DocumentChunk) + .where(DocumentChunk.user_id == user_id) + .where(DocumentChunk.session_id == session_id) + .where(DocumentChunk.deleted_at.is_(None)) + .order_by(distance) + .limit(limit) + ) + + result = await self.session.execute(stmt) + document_chunks = list(result.scalars().all()) + + logger.debug( + "Document chunks search completed: count={}, user_id={}, " + "session_id={}, limit={}", + len(document_chunks), + user_id, + session_id, + limit, + ) + + return document_chunks + + async def update(self, document_chunk: DocumentChunk) -> DocumentChunk: + """Update an existing document chunk in the database. + + Args: + document_chunk (DocumentChunk): Document chunk instance with updated + field values. + + Returns: + DocumentChunk: Updated and refreshed document chunk instance. + """ + await self.session.flush() + await self.session.refresh(document_chunk) + + logger.info("Document chunk updated: id={}", document_chunk.id) + + return document_chunk + + async def soft_delete_for_document(self, document_id: UUID) -> None: + """Soft-delete all document chunks of a document. + + Sets the deletion timestamp for every non-deleted chunk of the given + document instead of removing rows from the database. + + Args: + document_id (UUID): Unique document identifier. + """ + await self.session.execute( + update(DocumentChunk) + .where(DocumentChunk.document_id == document_id) + .where(DocumentChunk.deleted_at.is_(None)) + .values(deleted_at=datetime.now(UTC)) + ) + + await self.session.flush() + + logger.info("Document chunks soft-deleted: document_id={}", document_id) From cb14f210571b43a28cdbeba1c853b2cd5c952d39 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:27:54 +0300 Subject: [PATCH 25/81] feat(repository): update init file --- src/ai_notes_api/repositories/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ai_notes_api/repositories/__init__.py b/src/ai_notes_api/repositories/__init__.py index 7efafde..6392fdb 100644 --- a/src/ai_notes_api/repositories/__init__.py +++ b/src/ai_notes_api/repositories/__init__.py @@ -6,6 +6,8 @@ from .base import BaseRepository from .chat_memory import ChatMemoryRepository from .chat_session import ChatSessionRepository +from .document import DocumentRepository +from .document_chunk import DocumentChunkRepository from .filters import ( ChatSessionListFilters, GenerationJobListFilters, @@ -15,6 +17,8 @@ from .generation_job import GenerationJobRepository from .message import MessageRepository from .note import NoteRepository +from .rag_query import RagQueryRepository +from .rag_query_source import RagQuerySourceRepository from .user import UserRepository __all__ = [ @@ -29,4 +33,8 @@ "NoteRepository", "UserRepository", "ChatMemoryRepository", + "DocumentRepository", + "DocumentChunkRepository", + "RagQueryRepository", + "RagQuerySourceRepository", ] From 1f9a40df7114c5ad5985086ec6a50629b016464a Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:35:01 +0300 Subject: [PATCH 26/81] style(repository): rename var in vector_search_in_user_session --- src/ai_notes_api/repositories/document_chunk.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ai_notes_api/repositories/document_chunk.py b/src/ai_notes_api/repositories/document_chunk.py index d087800..8d91e81 100644 --- a/src/ai_notes_api/repositories/document_chunk.py +++ b/src/ai_notes_api/repositories/document_chunk.py @@ -118,12 +118,12 @@ async def get_list_for_document(self, document_id: UUID) -> list[DocumentChunk]: return document_chunks - async def search_in_user_session( + async def vector_search_in_user_session( self, query_embedding: list[float], user_id: UUID, session_id: UUID, - limit: int = 5, + top_k: int = 5, ) -> list[DocumentChunk]: """Return the most similar document chunks in a user's chat session. @@ -132,7 +132,7 @@ async def search_in_user_session( chunk embeddings against. user_id (UUID): Unique identifier of the user who owns the chunks. session_id (UUID): Unique chat session identifier. - limit (int): Maximum number of chunks to return. + top_k (int): Maximum number of chunks to return. Returns: list[DocumentChunk]: List of matching non-deleted document chunks @@ -146,7 +146,7 @@ async def search_in_user_session( .where(DocumentChunk.session_id == session_id) .where(DocumentChunk.deleted_at.is_(None)) .order_by(distance) - .limit(limit) + .limit(top_k) ) result = await self.session.execute(stmt) @@ -154,11 +154,11 @@ async def search_in_user_session( logger.debug( "Document chunks search completed: count={}, user_id={}, " - "session_id={}, limit={}", + "session_id={}, top_k={}", len(document_chunks), user_id, session_id, - limit, + top_k, ) return document_chunks From 90e020fbb69c1ce7be40d17815207bd066f89f84 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:36:18 +0300 Subject: [PATCH 27/81] test(repository): add document repository tests --- .../repositories/test_document_repository.py | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 tests/repositories/test_document_repository.py diff --git a/tests/repositories/test_document_repository.py b/tests/repositories/test_document_repository.py new file mode 100644 index 0000000..fd82429 --- /dev/null +++ b/tests/repositories/test_document_repository.py @@ -0,0 +1,365 @@ +"""Tests for document repository.""" + +from datetime import UTC, datetime, timedelta +from uuid import UUID, uuid4 + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from ai_notes_api.db.models import ( + ChatSession, + Document, + DocumentChunk, + DocumentStatus, + User, +) +from ai_notes_api.repositories.document import DocumentRepository + + +@pytest_asyncio.fixture +async def test_user(async_session: AsyncSession) -> User: + """Create a test user.""" + user = User( + email="test-user@example.com", + username="test_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +@pytest_asyncio.fixture +async def other_user(async_session: AsyncSession) -> User: + """Create another test user.""" + user = User( + email="other-user@example.com", + username="other_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +async def create_chat_session( + async_session: AsyncSession, + *, + user_id: UUID, + title: str = "Test chat session", +) -> ChatSession: + """Persist a chat session for document repository tests. + + Args: + async_session (AsyncSession): Database session used to persist the row. + user_id (UUID): Identifier of the user who owns the chat session. + title (str): Chat session title. + + Returns: + ChatSession: Persisted chat session instance. + """ + chat_session = ChatSession(user_id=user_id, title=title) + + async_session.add(chat_session) + await async_session.flush() + await async_session.refresh(chat_session) + + return chat_session + + +def create_document( + *, + user_id: UUID, + session_id: UUID, + filename: str = "test.pdf", + status: DocumentStatus = DocumentStatus.UPLOADED, + created_at: datetime | None = None, +) -> Document: + """Create a document instance for repository tests. + + Args: + user_id (UUID): Identifier of the user who owns the document. + session_id (UUID): Identifier of the chat session that owns the document. + filename (str): Original document file name. + status (DocumentStatus): Document processing status. + created_at (datetime | None): Optional explicit creation timestamp used to + control document ordering in tests. + + Returns: + Document: Document model instance. + """ + document = Document( + user_id=user_id, + session_id=session_id, + filename=filename, + content_type="application/pdf", + file_size=1024, + checksum_sha256="checksum", + storage_bucket="documents", + storage_object_name="object", + status=status, + ) + + if created_at is not None: + document.created_at = created_at + + return document + + +@pytest.mark.asyncio +async def test_create_document_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful document creation.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + document = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + + assert document.id is not None + assert document.user_id == test_user.id + assert document.session_id == chat_session.id + assert document.status == DocumentStatus.UPLOADED + + +@pytest.mark.asyncio +async def test_get_by_id_document_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful document retrieval by identifier.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + created = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + + document = await repository.get_by_id(created.id) + + assert document is not None + assert document.id == created.id + + +@pytest.mark.asyncio +async def test_get_by_id_document_not_found(async_session: AsyncSession) -> None: + """Test that document retrieval by identifier returns None when missing.""" + repository = DocumentRepository(session=async_session) + + document = await repository.get_by_id(uuid4()) + + assert document is None + + +@pytest.mark.asyncio +async def test_get_by_id_document_excludes_soft_deleted( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that document retrieval by identifier ignores soft-deleted rows.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + created = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + await repository.soft_delete(created) + + document = await repository.get_by_id(created.id) + + assert document is None + + +@pytest.mark.asyncio +async def test_get_by_id_for_user_document_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful document retrieval scoped to the owning user.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + created = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + + document = await repository.get_by_id_for_user(test_user.id, created.id) + + assert document is not None + assert document.id == created.id + + +@pytest.mark.asyncio +async def test_get_by_id_for_user_document_other_user_cannot_access( + async_session: AsyncSession, + test_user: User, + other_user: User, +) -> None: + """Test that another user cannot access a document by identifier.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + created = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + + document = await repository.get_by_id_for_user(other_user.id, created.id) + + assert document is None + + +@pytest.mark.asyncio +async def test_get_list_for_session_orders_by_created_at_desc( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that documents list is ordered by creation date in descending order.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + base = datetime.now(UTC) + + await repository.create( + create_document( + user_id=test_user.id, + session_id=chat_session.id, + filename="first.pdf", + created_at=base, + ) + ) + await repository.create( + create_document( + user_id=test_user.id, + session_id=chat_session.id, + filename="second.pdf", + created_at=base + timedelta(seconds=1), + ) + ) + + documents = await repository.get_list_for_session(test_user.id, chat_session.id) + + assert [document.filename for document in documents] == [ + "second.pdf", + "first.pdf", + ] + + +@pytest.mark.asyncio +async def test_get_list_for_session_scoped_to_user_and_session( + async_session: AsyncSession, + test_user: User, + other_user: User, +) -> None: + """Test that documents list is scoped to the requested user and session.""" + repository = DocumentRepository(session=async_session) + owned_session = await create_chat_session(async_session, user_id=test_user.id) + other_session = await create_chat_session(async_session, user_id=other_user.id) + + owned = await repository.create( + create_document(user_id=test_user.id, session_id=owned_session.id) + ) + await repository.create( + create_document(user_id=other_user.id, session_id=other_session.id) + ) + + documents = await repository.get_list_for_session(test_user.id, owned_session.id) + + assert len(documents) == 1 + assert documents[0].id == owned.id + + +@pytest.mark.asyncio +async def test_get_list_for_session_excludes_soft_deleted( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that documents list ignores soft-deleted rows.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + kept = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + deleted = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + await repository.soft_delete(deleted) + + documents = await repository.get_list_for_session(test_user.id, chat_session.id) + + assert len(documents) == 1 + assert documents[0].id == kept.id + + +@pytest.mark.asyncio +async def test_update_document_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful document update.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + document = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + + document.status = DocumentStatus.READY + + updated = await repository.update(document) + + assert updated.status == DocumentStatus.READY + + found = await repository.get_by_id(document.id) + + assert found is not None + assert found.status == DocumentStatus.READY + + +@pytest.mark.asyncio +async def test_soft_delete_document_cascades_to_chunks( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that soft-deleting a document also soft-deletes its chunks.""" + repository = DocumentRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + document = await repository.create( + create_document(user_id=test_user.id, session_id=chat_session.id) + ) + + chunk = DocumentChunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + chunk_index=0, + content="chunk", + content_hash="hash", + embedding=[0.0] * 1536, + embedding_model="text-embedding-3-small", + ) + async_session.add(chunk) + await async_session.flush() + + await repository.soft_delete(document) + + await async_session.refresh(document) + await async_session.refresh(chunk) + + assert document.deleted_at is not None + assert chunk.deleted_at is not None From 4f8d5e60064fde76f70719fa060ccc02f0c20754 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:36:43 +0300 Subject: [PATCH 28/81] test(repository): add document chunk repository tests --- .../test_document_chunk_repository.py | 510 ++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 tests/repositories/test_document_chunk_repository.py diff --git a/tests/repositories/test_document_chunk_repository.py b/tests/repositories/test_document_chunk_repository.py new file mode 100644 index 0000000..eab86ad --- /dev/null +++ b/tests/repositories/test_document_chunk_repository.py @@ -0,0 +1,510 @@ +"""Tests for document chunk repository.""" + +from uuid import UUID, uuid4 + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from ai_notes_api.db.models import ( + ChatSession, + Document, + DocumentChunk, + DocumentStatus, + User, +) +from ai_notes_api.repositories.document_chunk import DocumentChunkRepository + +EMBEDDING_DIM = 1536 + + +def make_embedding(*, first: float = 0.0, second: float = 0.0) -> list[float]: + """Build a fixed-size embedding with the first two components set. + + Args: + first (float): Value of the first embedding component. + second (float): Value of the second embedding component. + + Returns: + list[float]: Embedding vector of length ``EMBEDDING_DIM``. + """ + embedding = [0.0] * EMBEDDING_DIM + embedding[0] = first + embedding[1] = second + + return embedding + + +@pytest_asyncio.fixture +async def test_user(async_session: AsyncSession) -> User: + """Create a test user.""" + user = User( + email="test-user@example.com", + username="test_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +@pytest_asyncio.fixture +async def other_user(async_session: AsyncSession) -> User: + """Create another test user.""" + user = User( + email="other-user@example.com", + username="other_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +async def create_chat_session( + async_session: AsyncSession, + *, + user_id: UUID, +) -> ChatSession: + """Persist a chat session for document chunk repository tests. + + Args: + async_session (AsyncSession): Database session used to persist the row. + user_id (UUID): Identifier of the user who owns the chat session. + + Returns: + ChatSession: Persisted chat session instance. + """ + chat_session = ChatSession(user_id=user_id, title="Test chat session") + + async_session.add(chat_session) + await async_session.flush() + await async_session.refresh(chat_session) + + return chat_session + + +async def create_document( + async_session: AsyncSession, + *, + user_id: UUID, + session_id: UUID, +) -> Document: + """Persist a document for document chunk repository tests. + + Args: + async_session (AsyncSession): Database session used to persist the row. + user_id (UUID): Identifier of the user who owns the document. + session_id (UUID): Identifier of the chat session that owns the document. + + Returns: + Document: Persisted document instance. + """ + document = Document( + user_id=user_id, + session_id=session_id, + filename="test.pdf", + content_type="application/pdf", + file_size=1024, + checksum_sha256="checksum", + storage_bucket="documents", + storage_object_name="object", + status=DocumentStatus.READY, + ) + + async_session.add(document) + await async_session.flush() + await async_session.refresh(document) + + return document + + +def create_chunk( # noqa: PLR0913 + *, + user_id: UUID, + session_id: UUID, + document_id: UUID, + chunk_index: int = 0, + content: str = "chunk content", + embedding: list[float] | None = None, +) -> DocumentChunk: + """Create a document chunk instance for repository tests. + + Args: + user_id (UUID): Identifier of the user who owns the chunk. + session_id (UUID): Identifier of the chat session that owns the chunk. + document_id (UUID): Identifier of the document the chunk belongs to. + chunk_index (int): Position of the chunk within the document. + content (str): Text content of the chunk. + embedding (list[float] | None): Optional embedding vector. + + Returns: + DocumentChunk: Document chunk model instance. + """ + return DocumentChunk( + user_id=user_id, + session_id=session_id, + document_id=document_id, + chunk_index=chunk_index, + content=content, + content_hash=f"hash-{chunk_index}", + embedding=embedding if embedding is not None else make_embedding(first=1.0), + embedding_model="text-embedding-3-small", + ) + + +@pytest.mark.asyncio +async def test_create_chunk_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful document chunk creation.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + chunk = await repository.create( + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + ) + ) + + assert chunk.id is not None + assert chunk.document_id == document.id + + +@pytest.mark.asyncio +async def test_create_many_chunks_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful bulk document chunk creation.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + chunks = await repository.create_many( + [ + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + chunk_index=index, + ) + for index in range(3) + ] + ) + + assert len(chunks) == 3 + assert all(chunk.id is not None for chunk in chunks) + + +@pytest.mark.asyncio +async def test_get_by_id_chunk_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful document chunk retrieval by identifier.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + created = await repository.create( + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + ) + ) + + chunk = await repository.get_by_id(created.id) + + assert chunk is not None + assert chunk.id == created.id + + +@pytest.mark.asyncio +async def test_get_by_id_chunk_not_found(async_session: AsyncSession) -> None: + """Test that document chunk retrieval by identifier returns None when missing.""" + repository = DocumentChunkRepository(session=async_session) + + chunk = await repository.get_by_id(uuid4()) + + assert chunk is None + + +@pytest.mark.asyncio +async def test_get_list_for_document_orders_by_chunk_index_asc( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that chunks list is ordered by chunk index in ascending order.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + await repository.create_many( + [ + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + chunk_index=index, + ) + for index in (2, 0, 1) + ] + ) + + chunks = await repository.get_list_for_document(document.id) + + assert [chunk.chunk_index for chunk in chunks] == [0, 1, 2] + + +@pytest.mark.asyncio +async def test_get_list_for_document_excludes_soft_deleted( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that chunks list ignores soft-deleted rows.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + await repository.create( + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + ) + ) + await repository.soft_delete_for_document(document.id) + + chunks = await repository.get_list_for_document(document.id) + + assert chunks == [] + + +@pytest.mark.asyncio +async def test_search_in_user_session_orders_by_similarity( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that search returns chunks ordered by cosine distance to the query.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + near = await repository.create( + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + chunk_index=0, + content="near", + embedding=make_embedding(first=1.0), + ) + ) + far = await repository.create( + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + chunk_index=1, + content="far", + embedding=make_embedding(second=1.0), + ) + ) + + results = await repository.vector_search_in_user_session( + query_embedding=make_embedding(first=1.0), + user_id=test_user.id, + session_id=chat_session.id, + ) + + assert [chunk.id for chunk in results] == [near.id, far.id] + + +@pytest.mark.asyncio +async def test_search_in_user_session_respects_top_k( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that search limits the number of returned chunks to top_k.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + await repository.create_many( + [ + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + chunk_index=index, + ) + for index in range(3) + ] + ) + + results = await repository.vector_search_in_user_session( + query_embedding=make_embedding(first=1.0), + user_id=test_user.id, + session_id=chat_session.id, + top_k=2, + ) + + assert len(results) == 2 + + +@pytest.mark.asyncio +async def test_search_in_user_session_scoped_to_user_and_session( + async_session: AsyncSession, + test_user: User, + other_user: User, +) -> None: + """Test that search is scoped to the requested user and chat session.""" + repository = DocumentChunkRepository(session=async_session) + + owned_session = await create_chat_session(async_session, user_id=test_user.id) + owned_document = await create_document( + async_session, + user_id=test_user.id, + session_id=owned_session.id, + ) + owned = await repository.create( + create_chunk( + user_id=test_user.id, + session_id=owned_session.id, + document_id=owned_document.id, + ) + ) + + other_session = await create_chat_session(async_session, user_id=other_user.id) + other_document = await create_document( + async_session, + user_id=other_user.id, + session_id=other_session.id, + ) + await repository.create( + create_chunk( + user_id=other_user.id, + session_id=other_session.id, + document_id=other_document.id, + ) + ) + + results = await repository.vector_search_in_user_session( + query_embedding=make_embedding(first=1.0), + user_id=test_user.id, + session_id=owned_session.id, + ) + + assert len(results) == 1 + assert results[0].id == owned.id + + +@pytest.mark.asyncio +async def test_search_in_user_session_excludes_soft_deleted( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that search ignores soft-deleted chunks.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + await repository.create( + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + ) + ) + await repository.soft_delete_for_document(document.id) + + results = await repository.vector_search_in_user_session( + query_embedding=make_embedding(first=1.0), + user_id=test_user.id, + session_id=chat_session.id, + ) + + assert results == [] + + +@pytest.mark.asyncio +async def test_update_chunk_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful document chunk update.""" + repository = DocumentChunkRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + document = await create_document( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + chunk = await repository.create( + create_chunk( + user_id=test_user.id, + session_id=chat_session.id, + document_id=document.id, + content="original", + ) + ) + + chunk.content = "updated" + + updated = await repository.update(chunk) + + assert updated.content == "updated" + + found = await repository.get_by_id(chunk.id) + + assert found is not None + assert found.content == "updated" From fb252ef7e0677d2aa0494c00be38822eeb99e6da Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:37:05 +0300 Subject: [PATCH 29/81] test(repository): add rag query repository tests --- .../repositories/test_rag_query_repository.py | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 tests/repositories/test_rag_query_repository.py diff --git a/tests/repositories/test_rag_query_repository.py b/tests/repositories/test_rag_query_repository.py new file mode 100644 index 0000000..e2b776f --- /dev/null +++ b/tests/repositories/test_rag_query_repository.py @@ -0,0 +1,281 @@ +"""Tests for RAG query repository.""" + +from datetime import UTC, datetime, timedelta +from uuid import UUID, uuid4 + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from ai_notes_api.db.models import ChatSession, RagQuery, RagQueryStatus, User +from ai_notes_api.repositories.rag_query import RagQueryRepository + + +@pytest_asyncio.fixture +async def test_user(async_session: AsyncSession) -> User: + """Create a test user.""" + user = User( + email="test-user@example.com", + username="test_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +@pytest_asyncio.fixture +async def other_user(async_session: AsyncSession) -> User: + """Create another test user.""" + user = User( + email="other-user@example.com", + username="other_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +async def create_chat_session( + async_session: AsyncSession, + *, + user_id: UUID, +) -> ChatSession: + """Persist a chat session for RAG query repository tests. + + Args: + async_session (AsyncSession): Database session used to persist the row. + user_id (UUID): Identifier of the user who owns the chat session. + + Returns: + ChatSession: Persisted chat session instance. + """ + chat_session = ChatSession(user_id=user_id, title="Test chat session") + + async_session.add(chat_session) + await async_session.flush() + await async_session.refresh(chat_session) + + return chat_session + + +def create_rag_query( # noqa: PLR0913 + *, + user_id: UUID, + session_id: UUID, + question: str = "What is RAG?", + top_k: int = 5, + status: RagQueryStatus = RagQueryStatus.QUEUED, + created_at: datetime | None = None, +) -> RagQuery: + """Create a RAG query instance for repository tests. + + Args: + user_id (UUID): Identifier of the user who owns the RAG query. + session_id (UUID): Identifier of the chat session that owns the RAG query. + question (str): User question. + top_k (int): Number of chunks to retrieve. + status (RagQueryStatus): RAG query status. + created_at (datetime | None): Optional explicit creation timestamp used to + control RAG query ordering in tests. + + Returns: + RagQuery: RAG query model instance. + """ + rag_query = RagQuery( + user_id=user_id, + session_id=session_id, + question=question, + top_k=top_k, + status=status, + ) + + if created_at is not None: + rag_query.created_at = created_at + + return rag_query + + +@pytest.mark.asyncio +async def test_create_rag_query_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful RAG query creation.""" + repository = RagQueryRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + rag_query = await repository.create( + create_rag_query(user_id=test_user.id, session_id=chat_session.id) + ) + + assert rag_query.id is not None + assert rag_query.user_id == test_user.id + assert rag_query.session_id == chat_session.id + assert rag_query.status == RagQueryStatus.QUEUED + + +@pytest.mark.asyncio +async def test_get_by_id_rag_query_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful RAG query retrieval by identifier.""" + repository = RagQueryRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + created = await repository.create( + create_rag_query(user_id=test_user.id, session_id=chat_session.id) + ) + + rag_query = await repository.get_by_id(created.id) + + assert rag_query is not None + assert rag_query.id == created.id + + +@pytest.mark.asyncio +async def test_get_by_id_rag_query_not_found(async_session: AsyncSession) -> None: + """Test that RAG query retrieval by identifier returns None when missing.""" + repository = RagQueryRepository(session=async_session) + + rag_query = await repository.get_by_id(uuid4()) + + assert rag_query is None + + +@pytest.mark.asyncio +async def test_get_by_id_for_user_rag_query_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful RAG query retrieval scoped to the owning user.""" + repository = RagQueryRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + created = await repository.create( + create_rag_query(user_id=test_user.id, session_id=chat_session.id) + ) + + rag_query = await repository.get_by_id_for_user(test_user.id, created.id) + + assert rag_query is not None + assert rag_query.id == created.id + + +@pytest.mark.asyncio +async def test_get_by_id_for_user_rag_query_other_user_cannot_access( + async_session: AsyncSession, + test_user: User, + other_user: User, +) -> None: + """Test that another user cannot access a RAG query by identifier.""" + repository = RagQueryRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + created = await repository.create( + create_rag_query(user_id=test_user.id, session_id=chat_session.id) + ) + + rag_query = await repository.get_by_id_for_user(other_user.id, created.id) + + assert rag_query is None + + +@pytest.mark.asyncio +async def test_get_list_for_session_orders_by_created_at_desc( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that RAG queries list is ordered by creation date in descending order.""" + repository = RagQueryRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + base = datetime.now(UTC) + + await repository.create( + create_rag_query( + user_id=test_user.id, + session_id=chat_session.id, + question="First", + created_at=base, + ) + ) + await repository.create( + create_rag_query( + user_id=test_user.id, + session_id=chat_session.id, + question="Second", + created_at=base + timedelta(seconds=1), + ) + ) + + rag_queries = await repository.get_list_for_session(test_user.id, chat_session.id) + + assert [rag_query.question for rag_query in rag_queries] == ["Second", "First"] + + +@pytest.mark.asyncio +async def test_get_list_for_session_scoped_to_user_and_session( + async_session: AsyncSession, + test_user: User, + other_user: User, +) -> None: + """Test that RAG queries list is scoped to the requested user and session.""" + repository = RagQueryRepository(session=async_session) + owned_session = await create_chat_session(async_session, user_id=test_user.id) + other_session = await create_chat_session(async_session, user_id=other_user.id) + + owned = await repository.create( + create_rag_query(user_id=test_user.id, session_id=owned_session.id) + ) + await repository.create( + create_rag_query(user_id=other_user.id, session_id=other_session.id) + ) + + rag_queries = await repository.get_list_for_session(test_user.id, owned_session.id) + + assert len(rag_queries) == 1 + assert rag_queries[0].id == owned.id + + +@pytest.mark.asyncio +async def test_update_rag_query_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful RAG query update.""" + repository = RagQueryRepository(session=async_session) + chat_session = await create_chat_session(async_session, user_id=test_user.id) + + rag_query = await repository.create( + create_rag_query( + user_id=test_user.id, + session_id=chat_session.id, + status=RagQueryStatus.QUEUED, + ) + ) + + rag_query.status = RagQueryStatus.COMPLETED + rag_query.answer = "RAG is retrieval-augmented generation." + + updated = await repository.update(rag_query) + + assert updated.status == RagQueryStatus.COMPLETED + assert updated.answer == "RAG is retrieval-augmented generation." + + found = await repository.get_by_id(rag_query.id) + + assert found is not None + assert found.status == RagQueryStatus.COMPLETED From 7ba5d8da647e2ab148fe8dda3565e104241dab79 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:37:29 +0300 Subject: [PATCH 30/81] test(repository): add rag query source repository tests --- .../test_rag_query_source_repository.py | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 tests/repositories/test_rag_query_source_repository.py diff --git a/tests/repositories/test_rag_query_source_repository.py b/tests/repositories/test_rag_query_source_repository.py new file mode 100644 index 0000000..ca99c0f --- /dev/null +++ b/tests/repositories/test_rag_query_source_repository.py @@ -0,0 +1,316 @@ +"""Tests for RAG query source repository.""" + +from uuid import UUID + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from ai_notes_api.db.models import ( + ChatSession, + Document, + DocumentChunk, + DocumentStatus, + RagQuery, + RagQuerySource, + RagQueryStatus, + User, +) +from ai_notes_api.repositories.rag_query_source import RagQuerySourceRepository + +EMBEDDING_DIM = 1536 + + +@pytest_asyncio.fixture +async def test_user(async_session: AsyncSession) -> User: + """Create a test user.""" + user = User( + email="test-user@example.com", + username="test_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +async def create_rag_query( + async_session: AsyncSession, + *, + user_id: UUID, + session_id: UUID, +) -> RagQuery: + """Persist a RAG query for RAG query source repository tests. + + Args: + async_session (AsyncSession): Database session used to persist the row. + user_id (UUID): Identifier of the user who owns the RAG query. + session_id (UUID): Identifier of the chat session that owns the RAG query. + + Returns: + RagQuery: Persisted RAG query instance. + """ + rag_query = RagQuery( + user_id=user_id, + session_id=session_id, + question="What is RAG?", + top_k=5, + status=RagQueryStatus.COMPLETED, + ) + + async_session.add(rag_query) + await async_session.flush() + await async_session.refresh(rag_query) + + return rag_query + + +async def create_document_with_chunk( + async_session: AsyncSession, + *, + user_id: UUID, + session_id: UUID, +) -> tuple[Document, DocumentChunk]: + """Persist a document and a chunk for RAG query source repository tests. + + Args: + async_session (AsyncSession): Database session used to persist the rows. + user_id (UUID): Identifier of the user who owns the rows. + session_id (UUID): Identifier of the chat session that owns the rows. + + Returns: + tuple[Document, DocumentChunk]: Persisted document and chunk instances. + """ + document = Document( + user_id=user_id, + session_id=session_id, + filename="test.pdf", + content_type="application/pdf", + file_size=1024, + checksum_sha256="checksum", + storage_bucket="documents", + storage_object_name="object", + status=DocumentStatus.READY, + ) + + async_session.add(document) + await async_session.flush() + await async_session.refresh(document) + + chunk = DocumentChunk( + user_id=user_id, + session_id=session_id, + document_id=document.id, + chunk_index=0, + content="chunk content", + content_hash="hash", + embedding=[0.0] * EMBEDDING_DIM, + embedding_model="text-embedding-3-small", + ) + + async_session.add(chunk) + await async_session.flush() + await async_session.refresh(chunk) + + return document, chunk + + +def create_source( + *, + rag_query_id: UUID, + document_id: UUID, + chunk_id: UUID, + rank: int = 1, + score: float = 0.9, +) -> RagQuerySource: + """Create a RAG query source instance for repository tests. + + Args: + rag_query_id (UUID): Identifier of the RAG query the source belongs to. + document_id (UUID): Identifier of the source document. + chunk_id (UUID): Identifier of the source document chunk. + rank (int): Rank of the chunk among the retrieved sources. + score (float): Relevance score of the chunk for the query. + + Returns: + RagQuerySource: RAG query source model instance. + """ + return RagQuerySource( + rag_query_id=rag_query_id, + document_id=document_id, + chunk_id=chunk_id, + rank=rank, + score=score, + content_preview="preview", + ) + + +@pytest.mark.asyncio +async def test_create_source_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful RAG query source creation.""" + repository = RagQuerySourceRepository(session=async_session) + + chat_session = ChatSession(user_id=test_user.id, title="Test chat session") + async_session.add(chat_session) + await async_session.flush() + + rag_query = await create_rag_query( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + document, chunk = await create_document_with_chunk( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + source = await repository.create( + create_source( + rag_query_id=rag_query.id, + document_id=document.id, + chunk_id=chunk.id, + ) + ) + + assert source.id is not None + assert source.rag_query_id == rag_query.id + assert source.document_id == document.id + assert source.chunk_id == chunk.id + + +@pytest.mark.asyncio +async def test_create_many_sources_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful bulk RAG query source creation.""" + repository = RagQuerySourceRepository(session=async_session) + + chat_session = ChatSession(user_id=test_user.id, title="Test chat session") + async_session.add(chat_session) + await async_session.flush() + + rag_query = await create_rag_query( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + document, chunk = await create_document_with_chunk( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + sources = await repository.create_many( + [ + create_source( + rag_query_id=rag_query.id, + document_id=document.id, + chunk_id=chunk.id, + rank=rank, + ) + for rank in range(1, 4) + ] + ) + + assert len(sources) == 3 + assert all(source.id is not None for source in sources) + + +@pytest.mark.asyncio +async def test_get_list_for_rag_query_orders_by_rank_asc( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that sources list is ordered by rank in ascending order.""" + repository = RagQuerySourceRepository(session=async_session) + + chat_session = ChatSession(user_id=test_user.id, title="Test chat session") + async_session.add(chat_session) + await async_session.flush() + + rag_query = await create_rag_query( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + document, chunk = await create_document_with_chunk( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + await repository.create_many( + [ + create_source( + rag_query_id=rag_query.id, + document_id=document.id, + chunk_id=chunk.id, + rank=rank, + ) + for rank in (3, 1, 2) + ] + ) + + sources = await repository.get_list_for_rag_query(rag_query.id) + + assert [source.rank for source in sources] == [1, 2, 3] + + +@pytest.mark.asyncio +async def test_get_list_for_rag_query_scoped_to_rag_query( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that sources list is scoped to the requested RAG query.""" + repository = RagQuerySourceRepository(session=async_session) + + chat_session = ChatSession(user_id=test_user.id, title="Test chat session") + async_session.add(chat_session) + await async_session.flush() + + document, chunk = await create_document_with_chunk( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + first_query = await create_rag_query( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + second_query = await create_rag_query( + async_session, + user_id=test_user.id, + session_id=chat_session.id, + ) + + owned = await repository.create( + create_source( + rag_query_id=first_query.id, + document_id=document.id, + chunk_id=chunk.id, + ) + ) + await repository.create( + create_source( + rag_query_id=second_query.id, + document_id=document.id, + chunk_id=chunk.id, + ) + ) + + sources = await repository.get_list_for_rag_query(first_query.id) + + assert len(sources) == 1 + assert sources[0].id == owned.id From e8d8a635856aa424272b7a2c33d978607b271bb8 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:40:58 +0300 Subject: [PATCH 31/81] build(docker): add MinIO --- compose.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/compose.yml b/compose.yml index 3fd0316..972d2d8 100644 --- a/compose.yml +++ b/compose.yml @@ -29,6 +29,25 @@ services: timeout: 5s retries: 10 + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + restart: unless-stopped + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: shrimp + MINIO_ROOT_PASSWORD: shrimp + volumes: + - ai_notes_minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 10s + api: build: . container_name: ai_notes_api @@ -47,6 +66,8 @@ services: condition: service_healthy redis: condition: service_healthy + minio: + condition: service_healthy worker: build: . @@ -60,11 +81,9 @@ services: CELERY_BROKER_URL: redis://redis:6379/0 CELERY_RESULT_BACKEND: redis://redis:6379/1 healthcheck: - test: - ["CMD", "celery", "-A", "ai_notes_api.workers.celery_app.celery_app", - "inspect", "ping", "-d", "celery@$${HOSTNAME}"] + test: ["CMD-SHELL", "celery -A ai_notes_api.workers.celery_app.celery_app inspect ping -d celery@$${HOSTNAME} --timeout=10"] interval: 30s - timeout: 10s + timeout: 15s start_period: 20s retries: 3 depends_on: @@ -72,6 +91,9 @@ services: condition: service_healthy redis: condition: service_healthy + minio: + condition: service_healthy volumes: ai_notes_pg_data: + ai_notes_minio_data: From 7d7840927f53c09d89801117c44cd22d33610269 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:41:11 +0300 Subject: [PATCH 32/81] build(uv): add minio --- pyproject.toml | 1 + uv.lock | 148 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3cd3787..0987ff5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "celery>=5.6.3", "redis>=8.0.0", "pgvector>=0.4.2", + "minio>=7.2.20", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index dc48fad..f2ae60b 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,8 @@ revision = 2 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version == '3.14.*'", + "python_full_version < '3.14'", ] [[package]] @@ -18,6 +19,7 @@ dependencies = [ { name = "fastapi" }, { name = "greenlet" }, { name = "loguru" }, + { name = "minio" }, { name = "openai" }, { name = "passlib" }, { name = "pgvector" }, @@ -58,6 +60,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.136.3" }, { name = "greenlet", specifier = ">=3.5.1" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "minio", specifier = ">=7.2.20" }, { name = "openai", specifier = ">=2.41.1" }, { name = "passlib", specifier = ">=1.7.4" }, { name = "pgvector", specifier = ">=0.4.2" }, @@ -163,6 +166,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + [[package]] name = "ast-serialize" version = "0.5.0" @@ -319,6 +365,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -1039,6 +1130,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "minio" +version = "7.2.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi" }, + { name = "certifi" }, + { name = "pycryptodome" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, +] + [[package]] name = "msgpack" version = "1.2.0" @@ -1364,6 +1471,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" From b62c667b81a23c9f836a137122d7bf967cc561be Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:48:19 +0300 Subject: [PATCH 33/81] feat(storage): add minio document storage service --- src/ai_notes_api/core/config.py | 14 ++ src/ai_notes_api/storage/__init__.py | 9 + src/ai_notes_api/storage/client.py | 15 ++ src/ai_notes_api/storage/document_storage.py | 163 +++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 src/ai_notes_api/storage/__init__.py create mode 100644 src/ai_notes_api/storage/client.py create mode 100644 src/ai_notes_api/storage/document_storage.py diff --git a/src/ai_notes_api/core/config.py b/src/ai_notes_api/core/config.py index 3803ab9..7359dbb 100644 --- a/src/ai_notes_api/core/config.py +++ b/src/ai_notes_api/core/config.py @@ -33,6 +33,13 @@ class Settings(BaseSettings): sent to the LLM. celery_broker_url (str): Celery broker URL. celery_result_backend (str): Celery result backend URL. + minio_endpoint (str): MinIO server endpoint (host and port). + minio_access_key (str): MinIO access key. + minio_secret_key (str): MinIO secret key. + minio_secure (bool): Whether to use HTTPS when connecting to MinIO. + minio_bucket_name (str): Name of the bucket used to store documents. + minio_presigned_url_expire_seconds (int): Lifetime of presigned document + URLs in seconds. log_format (str): Format string used by Loguru for log messages. database_url (str): Async PostgreSQL database connection URL. model_config (SettingsConfigDict): Pydantic settings configuration. @@ -63,6 +70,13 @@ class Settings(BaseSettings): celery_broker_url: str = Field(...) celery_result_backend: str = Field(...) + minio_endpoint: str = Field(...) + minio_access_key: str = Field(...) + minio_secret_key: str = Field(...) + minio_secure: bool = Field(default=False) + minio_bucket_name: str = Field(default="documents") + minio_presigned_url_expire_seconds: int = Field(default=3600) + log_format: str = ( "[{time:DD/MM/YY HH:mm:ss}] " "[{file}:{function}:{line}] " diff --git a/src/ai_notes_api/storage/__init__.py b/src/ai_notes_api/storage/__init__.py new file mode 100644 index 0000000..3179043 --- /dev/null +++ b/src/ai_notes_api/storage/__init__.py @@ -0,0 +1,9 @@ +"""Storage package. + +This package exports the shared MinIO client and document storage helper. +""" + +from ai_notes_api.storage.client import minio_client +from ai_notes_api.storage.document_storage import DocumentStorage + +__all__ = ["DocumentStorage", "minio_client"] diff --git a/src/ai_notes_api/storage/client.py b/src/ai_notes_api/storage/client.py new file mode 100644 index 0000000..5c5a40a --- /dev/null +++ b/src/ai_notes_api/storage/client.py @@ -0,0 +1,15 @@ +"""MinIO client module. + +This module defines a shared MinIO client configured from application settings. +""" + +from minio import Minio + +from ai_notes_api.core import settings + +minio_client = Minio( + endpoint=settings.minio_endpoint, + access_key=settings.minio_access_key, + secret_key=settings.minio_secret_key, + secure=settings.minio_secure, +) diff --git a/src/ai_notes_api/storage/document_storage.py b/src/ai_notes_api/storage/document_storage.py new file mode 100644 index 0000000..ca5a193 --- /dev/null +++ b/src/ai_notes_api/storage/document_storage.py @@ -0,0 +1,163 @@ +"""Document storage module. + +This module provides a storage helper for uploading, downloading, and deleting +documents in MinIO object storage. +""" + +from datetime import timedelta +from io import BytesIO +from uuid import UUID + +from loguru import logger +from minio import Minio + +from ai_notes_api.core import settings + + +class DocumentStorage: + """Object storage helper for documents. + + Args: + client (Minio): MinIO client used to perform object storage operations. + """ + + def __init__(self, client: Minio) -> None: + """Initialize the document storage helper. + + Args: + client (Minio): MinIO client used to perform object storage + operations. + """ + self.client = client + self.bucket = settings.minio_bucket_name + + def ensure_bucket(self) -> None: + """Create the storage bucket if it does not already exist.""" + if not self.client.bucket_exists(self.bucket): + self.client.make_bucket(self.bucket) + logger.info("Storage bucket created: bucket={}", self.bucket) + + def build_object_name( + self, + user_id: UUID, + document_id: UUID, + filename: str, + ) -> str: + """Build the object name used to store a document. + + Args: + user_id (UUID): Identifier of the user who owns the document. + document_id (UUID): Unique document identifier. + filename (str): Original document file name. + + Returns: + str: Object name within the storage bucket. + """ + return f"users/{user_id}/documents/{document_id}/original/{filename}" + + def upload_file( + self, + user_id: UUID, + document_id: UUID, + filename: str, + data: bytes, + content_type: str, + ) -> str: + """Upload a document to object storage. + + Args: + user_id (UUID): Identifier of the user who owns the document. + document_id (UUID): Unique document identifier. + filename (str): Original document file name. + data (bytes): Raw document content. + content_type (str): MIME type of the document. + + Returns: + str: Object name under which the document was stored. + """ + self.ensure_bucket() + + object_name = self.build_object_name( + user_id=user_id, + document_id=document_id, + filename=filename, + ) + + self.client.put_object( + bucket_name=self.bucket, + object_name=object_name, + data=BytesIO(data), + length=len(data), + content_type=content_type, + ) + + logger.info( + "Document uploaded: object_name={}, size={}", + object_name, + len(data), + ) + + return object_name + + def download_file(self, object_name: str) -> bytes: + """Download a document from object storage. + + Args: + object_name (str): Object name within the storage bucket. + + Returns: + bytes: Raw document content. + """ + response = self.client.get_object(self.bucket, object_name) + + try: + data = response.read() + finally: + response.close() + response.release_conn() + + logger.debug("Document downloaded: object_name={}", object_name) + + return data + + def get_presigned_download_url( + self, + object_name: str, + expires_in_seconds: int | None = None, + ) -> str: + """Build a presigned URL for downloading a document. + + Args: + object_name (str): Object name within the storage bucket. + expires_in_seconds (int | None): Optional URL lifetime in seconds. + Defaults to the configured presigned URL lifetime. + + Returns: + str: Presigned URL used to download the document. + """ + if expires_in_seconds is None: + expires_in_seconds = settings.minio_presigned_url_expire_seconds + + url = self.client.presigned_get_object( + bucket_name=self.bucket, + object_name=object_name, + expires=timedelta(seconds=expires_in_seconds), + ) + + logger.debug( + "Presigned download URL created: object_name={}, expires_in_seconds={}", + object_name, + expires_in_seconds, + ) + + return url + + def delete_file(self, object_name: str) -> None: + """Delete a document from object storage. + + Args: + object_name (str): Object name within the storage bucket. + """ + self.client.remove_object(self.bucket, object_name) + + logger.info("Document deleted: object_name={}", object_name) From 3bfb03bee0682234b3dac1095f6622da4b8b474c Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:48:59 +0300 Subject: [PATCH 34/81] docs(minio): update config --- .env.example | 8 ++++++++ README.md | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/.env.example b/.env.example index e0a7e5f..f52579c 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,11 @@ LLM_CONTEXT_MESSAGES_LIMIT=20 # Celery CELERY_BROKER_URL=redis://localhost:6379/0 CELERY_RESULT_BACKEND=redis://localhost:6379/1 + +# MinIO +MINIO_ENDPOINT=localhost:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_SECURE=false +MINIO_BUCKET_NAME=documents +MINIO_PRESIGNED_URL_EXPIRE_SECONDS=3600 diff --git a/README.md b/README.md index 8b141e3..12ad8ca 100755 --- a/README.md +++ b/README.md @@ -96,6 +96,12 @@ Required variables: * `LLM_CONTEXT_MESSAGES_LIMIT` - number of recent messages sent as context * `CELERY_BROKER_URL` - Redis URL for the Celery broker * `CELERY_RESULT_BACKEND` - Redis URL for the Celery result backend +* `MINIO_ENDPOINT` - MinIO server endpoint (host and port) +* `MINIO_ACCESS_KEY` - MinIO access key +* `MINIO_SECRET_KEY` - MinIO secret key +* `MINIO_SECURE` - `false` or `true`, whether to use HTTPS for MinIO +* `MINIO_BUCKET_NAME` - bucket used to store documents, default `documents` +* `MINIO_PRESIGNED_URL_EXPIRE_SECONDS` - presigned document URL lifetime in seconds The database connection URL is composed automatically from the `POSTGRES_*` values. From b6818bda5c0902a513e0dbebf4dab2357bfaef82 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:53:48 +0300 Subject: [PATCH 35/81] feat(schemas): add document, chunk and rag schemas --- src/ai_notes_api/schemas/__init__.py | 40 ++++++--- src/ai_notes_api/schemas/chunk.py | 37 ++++++++ src/ai_notes_api/schemas/document.py | 125 +++++++++++++++++++++++++++ src/ai_notes_api/schemas/rag.py | 81 +++++++++++++++++ 4 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 src/ai_notes_api/schemas/chunk.py create mode 100644 src/ai_notes_api/schemas/document.py create mode 100644 src/ai_notes_api/schemas/rag.py diff --git a/src/ai_notes_api/schemas/__init__.py b/src/ai_notes_api/schemas/__init__.py index df1acf2..6a22c8a 100644 --- a/src/ai_notes_api/schemas/__init__.py +++ b/src/ai_notes_api/schemas/__init__.py @@ -3,17 +3,26 @@ This package re-exports schema classes used by the API. """ -from .chat_memory import ChatMemoryResponseSchema -from .chat_session import ( +from ai_notes_api.schemas.chat_memory import ChatMemoryResponseSchema +from ai_notes_api.schemas.chat_session import ( ChatSessionCreateSchema, ChatSessionListQuerySchema, ChatSessionListResponseSchema, ChatSessionResponseSchema, ChatSessionUpdateSchema, ) -from .completion import ChatCompletionResponseSchema -from .error import ErrorResponseSchema -from .generation_job import ( +from ai_notes_api.schemas.chunk import DocumentChunkRead +from ai_notes_api.schemas.completion import ChatCompletionResponseSchema +from ai_notes_api.schemas.document import ( + DocumentDeleteResponse, + DocumentDownloadUrlResponse, + DocumentListResponse, + DocumentProcessResponse, + DocumentRead, + DocumentUploadResponse, +) +from ai_notes_api.schemas.error import ErrorResponseSchema +from ai_notes_api.schemas.generation_job import ( GenerationJobCreateSchema, GenerationJobListQuerySchema, GenerationJobListResponseSchema, @@ -21,23 +30,24 @@ GenerationJobStatus, GenerationJobUpdateSchema, ) -from .message import ( +from ai_notes_api.schemas.message import ( AssistantMessageCreateSchema, MessageListQuerySchema, MessageListResponseSchema, MessageResponseSchema, UserMessageCreateSchema, ) -from .note import ( +from ai_notes_api.schemas.note import ( NoteCreateSchema, NoteListQuerySchema, NoteListResponseSchema, NoteResponseSchema, NoteUpdateSchema, ) -from .status import StatusResponseSchema -from .token import TokenResponseSchema -from .user import UserCreateSchema, UserResponseSchema +from ai_notes_api.schemas.rag import RagQueryRequest, RagQueryResponse, RagSourceRead +from ai_notes_api.schemas.status import StatusResponseSchema +from ai_notes_api.schemas.token import TokenResponseSchema +from ai_notes_api.schemas.user import UserCreateSchema, UserResponseSchema __all__ = [ "AssistantMessageCreateSchema", @@ -68,4 +78,14 @@ "GenerationJobResponseSchema", "GenerationJobUpdateSchema", "ChatMemoryResponseSchema", + "DocumentRead", + "DocumentListResponse", + "DocumentUploadResponse", + "DocumentProcessResponse", + "DocumentDeleteResponse", + "DocumentDownloadUrlResponse", + "DocumentChunkRead", + "RagQueryRequest", + "RagSourceRead", + "RagQueryResponse", ] diff --git a/src/ai_notes_api/schemas/chunk.py b/src/ai_notes_api/schemas/chunk.py new file mode 100644 index 0000000..12fa7dc --- /dev/null +++ b/src/ai_notes_api/schemas/chunk.py @@ -0,0 +1,37 @@ +"""Document chunk schemas module. + +This module defines Pydantic schemas used for document chunk API responses. +""" + +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + + +class DocumentChunkRead(BaseModel): + """Schema for returning document chunk data. + + Attributes: + id (UUID): Unique document chunk identifier. + document_id (UUID): Unique document identifier. + chat_session_id (UUID): Unique chat session identifier. + chunk_index (int): Position of the chunk within the document. + content (str): Text content of the chunk. + embedding_model (str): Name of the model used to produce the embedding. + token_count (int | None): Optional number of tokens in the chunk. + created_at (datetime): Date and time when the chunk was created. + """ + + model_config = ConfigDict(from_attributes=True, populate_by_name=True) + + id: UUID + document_id: UUID + chat_session_id: UUID = Field(validation_alias="session_id") + + chunk_index: int + content: str + embedding_model: str + token_count: int | None = None + + created_at: datetime diff --git a/src/ai_notes_api/schemas/document.py b/src/ai_notes_api/schemas/document.py new file mode 100644 index 0000000..d07b3aa --- /dev/null +++ b/src/ai_notes_api/schemas/document.py @@ -0,0 +1,125 @@ +"""Document schemas module. + +This module defines Pydantic schemas used for document API requests and +responses. +""" + +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + +from ai_notes_api.db.models import DocumentStatus + + +class DocumentRead(BaseModel): + """Schema for returning document data. + + Attributes: + id (UUID): Unique document identifier. + chat_session_id (UUID): Unique chat session identifier. + filename (str): Original document file name. + content_type (str): MIME type of the document. + file_size (int): Document size in bytes. + checksum_sha256 (str): SHA-256 checksum of the document content. + status (DocumentStatus): Current document processing status. + error_message (str | None): Optional error message if document + processing failed. + created_at (datetime): Date and time when the document was created. + updated_at (datetime): Date and time when the document was last updated. + processed_at (datetime | None): Optional date and time when the document + finished processing. + """ + + model_config = ConfigDict(from_attributes=True, populate_by_name=True) + + id: UUID + chat_session_id: UUID = Field(validation_alias="session_id") + + filename: str + content_type: str + file_size: int + checksum_sha256: str + + status: DocumentStatus + error_message: str | None = None + + created_at: datetime + updated_at: datetime + processed_at: datetime | None = None + + +class DocumentListResponse(BaseModel): + """Schema for returning a paginated list of documents. + + Attributes: + items (list[DocumentRead]): List of documents. + limit (int): Maximum number of documents returned. + offset (int): Number of documents skipped before returning results. + total (int): Total number of documents in the current page. + """ + + items: list[DocumentRead] + limit: int + offset: int + total: int + + +class DocumentUploadResponse(BaseModel): + """Schema for returning the result of a document upload. + + Attributes: + id (UUID): Unique document identifier. + chat_session_id (UUID): Unique chat session identifier. + filename (str): Original document file name. + status (DocumentStatus): Current document processing status. + created_at (datetime): Date and time when the document was created. + """ + + model_config = ConfigDict(from_attributes=True, populate_by_name=True) + + id: UUID + chat_session_id: UUID = Field(validation_alias="session_id") + filename: str + status: DocumentStatus + created_at: datetime + + +class DocumentProcessResponse(BaseModel): + """Schema for returning the result of a document processing request. + + Attributes: + document_id (UUID): Unique document identifier. + status (DocumentStatus): Current document processing status. + message (str): Human-readable description of the processing result. + """ + + document_id: UUID + status: DocumentStatus + message: str + + +class DocumentDeleteResponse(BaseModel): + """Schema for returning the result of a document deletion. + + Attributes: + document_id (UUID): Unique document identifier. + status (DocumentStatus): Current document status. + message (str): Human-readable description of the deletion result. + """ + + document_id: UUID + status: DocumentStatus + message: str + + +class DocumentDownloadUrlResponse(BaseModel): + """Schema for returning a presigned document download URL. + + Attributes: + url (str): Presigned URL used to download the document. + expires_in_seconds (int): Number of seconds until the URL expires. + """ + + url: str + expires_in_seconds: int diff --git a/src/ai_notes_api/schemas/rag.py b/src/ai_notes_api/schemas/rag.py new file mode 100644 index 0000000..486e86d --- /dev/null +++ b/src/ai_notes_api/schemas/rag.py @@ -0,0 +1,81 @@ +"""RAG schemas module. + +This module defines Pydantic schemas used for RAG query API requests and +responses. +""" + +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + +from ai_notes_api.db.models import RagQueryStatus + + +class RagQueryRequest(BaseModel): + """Schema for creating a RAG query. + + Attributes: + question (str): User question. + top_k (int): Number of document chunks to retrieve for the query. + """ + + question: str = Field(min_length=1, max_length=10_000) + top_k: int = Field(default=5, ge=1, le=20) + + +class RagSourceRead(BaseModel): + """Schema for returning a RAG query source. + + Attributes: + document_id (UUID): Unique document identifier. + chunk_id (UUID): Unique document chunk identifier. + rank (int): Rank of the chunk among the retrieved sources. + score (float): Relevance score of the chunk for the query. + preview (str): Preview of the chunk content. + """ + + model_config = ConfigDict(from_attributes=True, populate_by_name=True) + + document_id: UUID + chunk_id: UUID + rank: int + score: float + preview: str = Field(validation_alias="content_preview") + + +class RagQueryResponse(BaseModel): + """Schema for returning RAG query data. + + Attributes: + id (UUID): Unique RAG query identifier. + chat_session_id (UUID): Unique chat session identifier. + question (str): User question. + answer (str | None): Optional generated answer. + provider (str | None): Optional AI provider name. + model (str | None): Optional AI model name. + top_k (int): Number of document chunks retrieved for the query. + status (RagQueryStatus): Current RAG query status. + sources (list[RagSourceRead]): Sources retrieved for the RAG query. + created_at (datetime): Date and time when the RAG query was created. + finished_at (datetime | None): Date and time when the RAG query finished. + """ + + model_config = ConfigDict(from_attributes=True, populate_by_name=True) + + id: UUID + chat_session_id: UUID = Field(validation_alias="session_id") + + question: str + answer: str | None = None + + provider: str | None = None + model: str | None = None + + top_k: int + status: RagQueryStatus + + sources: list[RagSourceRead] = Field(default_factory=list) + + created_at: datetime + finished_at: datetime | None = None From f96e68e9d5cec4be7b549646d188897cbb2b0a25 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 06:53:48 +0300 Subject: [PATCH 36/81] style(imports): use absolute imports in package init files --- src/ai_notes_api/api/v1/__init__.py | 2 +- src/ai_notes_api/core/__init__.py | 6 +++--- src/ai_notes_api/db/models/__init__.py | 24 ++++++++++----------- src/ai_notes_api/exceptions/__init__.py | 19 +++++++++------- src/ai_notes_api/llm/__init__.py | 6 +++--- src/ai_notes_api/memory/__init__.py | 6 +++--- src/ai_notes_api/repositories/__init__.py | 24 ++++++++++----------- src/ai_notes_api/services/__init__.py | 14 ++++++------ src/ai_notes_api/tools/__init__.py | 8 +++---- src/ai_notes_api/tools/builtins/__init__.py | 10 ++++----- 10 files changed, 61 insertions(+), 58 deletions(-) diff --git a/src/ai_notes_api/api/v1/__init__.py b/src/ai_notes_api/api/v1/__init__.py index d2a2f83..e87c23c 100644 --- a/src/ai_notes_api/api/v1/__init__.py +++ b/src/ai_notes_api/api/v1/__init__.py @@ -3,6 +3,6 @@ This package exposes the API v1 router for application-level routing. """ -from .router import router +from ai_notes_api.api.v1.router import router __all__ = ["router"] diff --git a/src/ai_notes_api/core/__init__.py b/src/ai_notes_api/core/__init__.py index 6d4fd95..c98486e 100644 --- a/src/ai_notes_api/core/__init__.py +++ b/src/ai_notes_api/core/__init__.py @@ -3,9 +3,9 @@ This package re-exports core application objects and utilities. """ -from .config import settings -from .logger import setup_logger -from .security import ( +from ai_notes_api.core.config import settings +from ai_notes_api.core.logger import setup_logger +from ai_notes_api.core.security import ( create_access_token, decode_access_token, hash_password, diff --git a/src/ai_notes_api/db/models/__init__.py b/src/ai_notes_api/db/models/__init__.py index df2fccd..29b423f 100644 --- a/src/ai_notes_api/db/models/__init__.py +++ b/src/ai_notes_api/db/models/__init__.py @@ -3,18 +3,18 @@ This package re-exports the base class used by SQLAlchemy ORM models. """ -from .base import Base -from .chat_memory import ChatMemory -from .chat_session import ChatSession, ChatSessionGenerationStatus -from .datetime import SoftDeleteMixin, TimestampMixin -from .document import Document, DocumentStatus -from .document_chunk import DocumentChunk -from .generation_job import GenerationJob, GenerationJobStatus -from .message import Message, MessageRole -from .note import ModelSource, Note -from .rag_query import RagQuery, RagQueryStatus -from .rag_query_source import RagQuerySource -from .user import User +from ai_notes_api.db.models.base import Base +from ai_notes_api.db.models.chat_memory import ChatMemory +from ai_notes_api.db.models.chat_session import ChatSession, ChatSessionGenerationStatus +from ai_notes_api.db.models.datetime import SoftDeleteMixin, TimestampMixin +from ai_notes_api.db.models.document import Document, DocumentStatus +from ai_notes_api.db.models.document_chunk import DocumentChunk +from ai_notes_api.db.models.generation_job import GenerationJob, GenerationJobStatus +from ai_notes_api.db.models.message import Message, MessageRole +from ai_notes_api.db.models.note import ModelSource, Note +from ai_notes_api.db.models.rag_query import RagQuery, RagQueryStatus +from ai_notes_api.db.models.rag_query_source import RagQuerySource +from ai_notes_api.db.models.user import User __all__ = [ "Base", diff --git a/src/ai_notes_api/exceptions/__init__.py b/src/ai_notes_api/exceptions/__init__.py index 0aab83b..2169305 100644 --- a/src/ai_notes_api/exceptions/__init__.py +++ b/src/ai_notes_api/exceptions/__init__.py @@ -4,18 +4,21 @@ registration utilities. """ -from .base import AppException, register_exception_handlers -from .chat_memory import ( +from ai_notes_api.exceptions.base import AppException, register_exception_handlers +from ai_notes_api.exceptions.chat_memory import ( ChatMemoryDependenciesNotConfiguredError, ChatMemoryNotFoundError, MemoryInProgressError, ) -from .chat_session import ChatSessionNotFoundError -from .generation_job import GenerationInProgressError, GenerationNotFoundError -from .message import MessageNotFoundError -from .note import NoteNotFoundError -from .token import InvalidTokenError -from .user import ( +from ai_notes_api.exceptions.chat_session import ChatSessionNotFoundError +from ai_notes_api.exceptions.generation_job import ( + GenerationInProgressError, + GenerationNotFoundError, +) +from ai_notes_api.exceptions.message import MessageNotFoundError +from ai_notes_api.exceptions.note import NoteNotFoundError +from ai_notes_api.exceptions.token import InvalidTokenError +from ai_notes_api.exceptions.user import ( InactiveUserError, InvalidCredentialsError, UserAlreadyExistsError, diff --git a/src/ai_notes_api/llm/__init__.py b/src/ai_notes_api/llm/__init__.py index 420e08a..ca9ba36 100644 --- a/src/ai_notes_api/llm/__init__.py +++ b/src/ai_notes_api/llm/__init__.py @@ -4,9 +4,9 @@ tools. """ -from .client import LLMClient -from .embeddings import EmbeddingClient -from .models import LLMMessage, LLMResponse, LLMStreamEvent, LLMToolCall +from ai_notes_api.llm.client import LLMClient +from ai_notes_api.llm.embeddings import EmbeddingClient +from ai_notes_api.llm.models import LLMMessage, LLMResponse, LLMStreamEvent, LLMToolCall __all__ = [ "LLMClient", diff --git a/src/ai_notes_api/memory/__init__.py b/src/ai_notes_api/memory/__init__.py index f4aaaa2..54dfda1 100644 --- a/src/ai_notes_api/memory/__init__.py +++ b/src/ai_notes_api/memory/__init__.py @@ -3,8 +3,8 @@ This package exports memory-related services. """ -from .extractor import MemoryExtractor -from .prompt_builder import PromptBuilder -from .summarizer import MemorySummarizer +from ai_notes_api.memory.extractor import MemoryExtractor +from ai_notes_api.memory.prompt_builder import PromptBuilder +from ai_notes_api.memory.summarizer import MemorySummarizer __all__ = ["MemorySummarizer", "MemoryExtractor", "PromptBuilder"] diff --git a/src/ai_notes_api/repositories/__init__.py b/src/ai_notes_api/repositories/__init__.py index 6392fdb..b0652a0 100644 --- a/src/ai_notes_api/repositories/__init__.py +++ b/src/ai_notes_api/repositories/__init__.py @@ -3,23 +3,23 @@ This package re-exports repository classes used for database access. """ -from .base import BaseRepository -from .chat_memory import ChatMemoryRepository -from .chat_session import ChatSessionRepository -from .document import DocumentRepository -from .document_chunk import DocumentChunkRepository -from .filters import ( +from ai_notes_api.repositories.base import BaseRepository +from ai_notes_api.repositories.chat_memory import ChatMemoryRepository +from ai_notes_api.repositories.chat_session import ChatSessionRepository +from ai_notes_api.repositories.document import DocumentRepository +from ai_notes_api.repositories.document_chunk import DocumentChunkRepository +from ai_notes_api.repositories.filters import ( ChatSessionListFilters, GenerationJobListFilters, MessageListFilters, NoteListFilters, ) -from .generation_job import GenerationJobRepository -from .message import MessageRepository -from .note import NoteRepository -from .rag_query import RagQueryRepository -from .rag_query_source import RagQuerySourceRepository -from .user import UserRepository +from ai_notes_api.repositories.generation_job import GenerationJobRepository +from ai_notes_api.repositories.message import MessageRepository +from ai_notes_api.repositories.note import NoteRepository +from ai_notes_api.repositories.rag_query import RagQueryRepository +from ai_notes_api.repositories.rag_query_source import RagQuerySourceRepository +from ai_notes_api.repositories.user import UserRepository __all__ = [ "BaseRepository", diff --git a/src/ai_notes_api/services/__init__.py b/src/ai_notes_api/services/__init__.py index 5fb998b..8a0ab67 100644 --- a/src/ai_notes_api/services/__init__.py +++ b/src/ai_notes_api/services/__init__.py @@ -3,13 +3,13 @@ This package re-exports application service classes. """ -from .auth import AuthService -from .chat_memory import ChatMemoryService -from .chat_session import ChatSessionService -from .generation_job import JobService -from .llm_service import LLMService -from .message import MessageService -from .note import NoteService +from ai_notes_api.services.auth import AuthService +from ai_notes_api.services.chat_memory import ChatMemoryService +from ai_notes_api.services.chat_session import ChatSessionService +from ai_notes_api.services.generation_job import JobService +from ai_notes_api.services.llm_service import LLMService +from ai_notes_api.services.message import MessageService +from ai_notes_api.services.note import NoteService __all__ = [ "AuthService", diff --git a/src/ai_notes_api/tools/__init__.py b/src/ai_notes_api/tools/__init__.py index c21db3a..b32e53c 100644 --- a/src/ai_notes_api/tools/__init__.py +++ b/src/ai_notes_api/tools/__init__.py @@ -4,10 +4,10 @@ exceptions. """ -from .exceptions import ToolAlreadyRegisteredError -from .factory import build_registry -from .models import ToolSpec -from .registry import ToolRegistry +from ai_notes_api.tools.exceptions import ToolAlreadyRegisteredError +from ai_notes_api.tools.factory import build_registry +from ai_notes_api.tools.models import ToolSpec +from ai_notes_api.tools.registry import ToolRegistry __all__ = [ "ToolRegistry", diff --git a/src/ai_notes_api/tools/builtins/__init__.py b/src/ai_notes_api/tools/builtins/__init__.py index e5485b5..f111c0e 100644 --- a/src/ai_notes_api/tools/builtins/__init__.py +++ b/src/ai_notes_api/tools/builtins/__init__.py @@ -3,11 +3,11 @@ This package exports built-in LLM tool factories. """ -from .create_note import make_create_note_tool -from .delete_note import make_delete_note_tool -from .get_note import make_get_note_by_id_tool -from .search_notes import make_search_notes_tool -from .update_note import make_update_note_tool +from ai_notes_api.tools.builtins.create_note import make_create_note_tool +from ai_notes_api.tools.builtins.delete_note import make_delete_note_tool +from ai_notes_api.tools.builtins.get_note import make_get_note_by_id_tool +from ai_notes_api.tools.builtins.search_notes import make_search_notes_tool +from ai_notes_api.tools.builtins.update_note import make_update_note_tool __all__ = [ "make_search_notes_tool", From 246e5e22a25d773de897b7b343c5b33262e7692e Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:05:56 +0300 Subject: [PATCH 37/81] build(uv): remove minio and add aioboto3 --- pyproject.toml | 2 +- uv.lock | 741 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 548 insertions(+), 195 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0987ff5..d72920f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "celery>=5.6.3", "redis>=8.0.0", "pgvector>=0.4.2", - "minio>=7.2.20", + "aioboto3>=15.5.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index f2ae60b..7c23793 100644 --- a/uv.lock +++ b/uv.lock @@ -12,6 +12,7 @@ name = "ai-notes-api" version = "0.4.0" source = { editable = "." } dependencies = [ + { name = "aioboto3" }, { name = "alembic" }, { name = "asyncpg" }, { name = "bcrypt" }, @@ -19,7 +20,6 @@ dependencies = [ { name = "fastapi" }, { name = "greenlet" }, { name = "loguru" }, - { name = "minio" }, { name = "openai" }, { name = "passlib" }, { name = "pgvector" }, @@ -53,6 +53,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aioboto3", specifier = ">=15.5.0" }, { name = "alembic", specifier = ">=1.18.4" }, { name = "asyncpg", specifier = ">=0.31.0" }, { name = "bcrypt", specifier = "==4.0.1" }, @@ -60,7 +61,6 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.136.3" }, { name = "greenlet", specifier = ">=3.5.1" }, { name = "loguru", specifier = ">=0.7.3" }, - { name = "minio", specifier = ">=7.2.20" }, { name = "openai", specifier = ">=2.41.1" }, { name = "passlib", specifier = ">=1.7.4" }, { name = "pgvector", specifier = ">=0.4.2" }, @@ -92,6 +92,162 @@ dev = [ { name = "types-python-jose", specifier = ">=3.5.0.20260408" }, ] +[[package]] +name = "aioboto3" +version = "15.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" }, +] + +[[package]] +name = "aiobotocore" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.1" +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/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "aiosqlite" version = "0.22.1" @@ -166,49 +322,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] -[[package]] -name = "argon2-cffi" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argon2-cffi-bindings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, -] - -[[package]] -name = "argon2-cffi-bindings" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, - { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, - { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, - { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, - { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, - { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, - { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, - { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, - { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, - { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, - { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, - { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, -] - [[package]] name = "ast-serialize" version = "0.5.0" @@ -281,6 +394,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "bcrypt" version = "4.0.1" @@ -318,6 +440,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, ] +[[package]] +name = "boto3" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, +] + [[package]] name = "cachecontrol" version = "0.14.4" @@ -365,51 +515,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - [[package]] name = "cfgv" version = "3.5.0" @@ -771,6 +876,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "greenlet" version = "3.5.2" @@ -958,6 +1136,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "kombu" version = "5.6.2" @@ -1130,22 +1317,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "minio" -version = "7.2.20" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argon2-cffi" }, - { name = "certifi" }, - { name = "pycryptodome" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, -] - [[package]] name = "msgpack" version = "1.2.0" @@ -1187,6 +1358,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/04/3fa2dffb87bf598696b86bde7cd642d0a7590520c3fa24cd19611dfebeb7/msgpack-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2825bb1da548d214ab8a810906b7dd69a10f3838b615a2cc46e5172d3cb44f6e", size = 71004, upload-time = "2026-06-11T04:16:09.556Z" }, ] +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + [[package]] name = "mypy" version = "2.1.0" @@ -1450,6 +1702,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + [[package]] name = "py-serializable" version = "2.1.0" @@ -1471,45 +1800,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pycryptodome" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, -] - [[package]] name = "pydantic" version = "2.13.4" @@ -1851,6 +2141,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, ] +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -2150,53 +2452,104 @@ wheels = [ [[package]] name = "wrapt" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, - { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, - { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, - { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, - { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, - { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, - { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, - { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, - { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, - { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, - { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, - { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, - { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, - { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" }, - { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" }, - { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" }, - { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" }, - { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" }, - { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" }, - { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" }, - { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" }, - { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" }, - { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" }, - { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" }, - { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" }, - { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" }, - { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] From acc6cc37483ad54eaae9d94bf1e27e81d7933106 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:06:19 +0300 Subject: [PATCH 38/81] refactor(config): update s3 config names --- .env.example | 14 +++++++------- README.md | 12 ++++++------ src/ai_notes_api/core/config.py | 24 ++++++++++++------------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index f52579c..b154150 100644 --- a/.env.example +++ b/.env.example @@ -27,10 +27,10 @@ LLM_CONTEXT_MESSAGES_LIMIT=20 CELERY_BROKER_URL=redis://localhost:6379/0 CELERY_RESULT_BACKEND=redis://localhost:6379/1 -# MinIO -MINIO_ENDPOINT=localhost:9000 -MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin -MINIO_SECURE=false -MINIO_BUCKET_NAME=documents -MINIO_PRESIGNED_URL_EXPIRE_SECONDS=3600 +# S3 +S3_ENDPOINT_URL=http://localhost:9000 +S3_ACCESS_KEY_ID=minioadmin +S3_SECRET_ACCESS_KEY=minioadmin +S3_REGION=us-east-1 +S3_BUCKET_NAME=documents +S3_PRESIGNED_URL_EXPIRE_SECONDS=3600 diff --git a/README.md b/README.md index 12ad8ca..4ccda24 100755 --- a/README.md +++ b/README.md @@ -96,12 +96,12 @@ Required variables: * `LLM_CONTEXT_MESSAGES_LIMIT` - number of recent messages sent as context * `CELERY_BROKER_URL` - Redis URL for the Celery broker * `CELERY_RESULT_BACKEND` - Redis URL for the Celery result backend -* `MINIO_ENDPOINT` - MinIO server endpoint (host and port) -* `MINIO_ACCESS_KEY` - MinIO access key -* `MINIO_SECRET_KEY` - MinIO secret key -* `MINIO_SECURE` - `false` or `true`, whether to use HTTPS for MinIO -* `MINIO_BUCKET_NAME` - bucket used to store documents, default `documents` -* `MINIO_PRESIGNED_URL_EXPIRE_SECONDS` - presigned document URL lifetime in seconds +* `S3_ENDPOINT_URL` - S3 endpoint URL +* `S3_ACCESS_KEY_ID` - S3 access key ID +* `S3_SECRET_ACCESS_KEY` - S3 secret access key +* `S3_REGION` - S3 region name, default `us-east-1` +* `S3_BUCKET_NAME` - bucket used to store documents, default `documents` +* `S3_PRESIGNED_URL_EXPIRE_SECONDS` - presigned document URL lifetime in seconds The database connection URL is composed automatically from the `POSTGRES_*` values. diff --git a/src/ai_notes_api/core/config.py b/src/ai_notes_api/core/config.py index 7359dbb..64bd324 100644 --- a/src/ai_notes_api/core/config.py +++ b/src/ai_notes_api/core/config.py @@ -33,12 +33,12 @@ class Settings(BaseSettings): sent to the LLM. celery_broker_url (str): Celery broker URL. celery_result_backend (str): Celery result backend URL. - minio_endpoint (str): MinIO server endpoint (host and port). - minio_access_key (str): MinIO access key. - minio_secret_key (str): MinIO secret key. - minio_secure (bool): Whether to use HTTPS when connecting to MinIO. - minio_bucket_name (str): Name of the bucket used to store documents. - minio_presigned_url_expire_seconds (int): Lifetime of presigned document + s3_endpoint_url (str): S3 endpoint URL. + s3_access_key_id (str): S3 access key ID. + s3_secret_access_key (str): S3 secret access key. + s3_region (str): S3 region name. + s3_bucket_name (str): Name of the bucket used to store documents. + s3_presigned_url_expire_seconds (int): Lifetime of presigned document URLs in seconds. log_format (str): Format string used by Loguru for log messages. database_url (str): Async PostgreSQL database connection URL. @@ -70,12 +70,12 @@ class Settings(BaseSettings): celery_broker_url: str = Field(...) celery_result_backend: str = Field(...) - minio_endpoint: str = Field(...) - minio_access_key: str = Field(...) - minio_secret_key: str = Field(...) - minio_secure: bool = Field(default=False) - minio_bucket_name: str = Field(default="documents") - minio_presigned_url_expire_seconds: int = Field(default=3600) + s3_endpoint_url: str = Field(...) + s3_access_key_id: str = Field(...) + s3_secret_access_key: str = Field(...) + s3_region: str = Field(default="us-east-1") + s3_bucket_name: str = Field(default="documents") + s3_presigned_url_expire_seconds: int = Field(default=3600) log_format: str = ( "[{time:DD/MM/YY HH:mm:ss}] " From b647d9c9b6a1a42db69c6131c959783869874801 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:09:16 +0300 Subject: [PATCH 39/81] build(mypy): add aioboto3 ignore in mypy --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d72920f..8023808 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ extend-include = ["*.ipynb"] exclude = [".git", "__pycache__", "build", "dist", ".venv", "tmp", "alembic"] [[tool.mypy.overrides]] -module = ["celery.*"] +module = ["celery.*", "aioboto3.*", "botocore.*"] ignore_missing_imports = true [tool.ruff.format] From eda89314f7a00bbca2d90535ad393344ad637d27 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:09:41 +0300 Subject: [PATCH 40/81] feat(storage): replcae minio with aioboto3 --- src/ai_notes_api/storage/__init__.py | 6 +- src/ai_notes_api/storage/client.py | 38 ++++++++--- src/ai_notes_api/storage/document_storage.py | 68 +++++++++++--------- 3 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/ai_notes_api/storage/__init__.py b/src/ai_notes_api/storage/__init__.py index 3179043..033d66b 100644 --- a/src/ai_notes_api/storage/__init__.py +++ b/src/ai_notes_api/storage/__init__.py @@ -1,9 +1,9 @@ """Storage package. -This package exports the shared MinIO client and document storage helper. +This package exports the shared S3 client factory and document storage helper. """ -from ai_notes_api.storage.client import minio_client +from ai_notes_api.storage.client import get_s3_client from ai_notes_api.storage.document_storage import DocumentStorage -__all__ = ["DocumentStorage", "minio_client"] +__all__ = ["DocumentStorage", "get_s3_client"] diff --git a/src/ai_notes_api/storage/client.py b/src/ai_notes_api/storage/client.py index 5c5a40a..bbf953a 100644 --- a/src/ai_notes_api/storage/client.py +++ b/src/ai_notes_api/storage/client.py @@ -1,15 +1,35 @@ -"""MinIO client module. +"""S3 client module. -This module defines a shared MinIO client configured from application settings. +This module defines an asynchronous S3 client factory configured from +application settings. """ -from minio import Minio +from collections.abc import AsyncIterator +from typing import Any + +import aioboto3 +from botocore.config import Config from ai_notes_api.core import settings -minio_client = Minio( - endpoint=settings.minio_endpoint, - access_key=settings.minio_access_key, - secret_key=settings.minio_secret_key, - secure=settings.minio_secure, -) + +async def get_s3_client() -> AsyncIterator[Any]: + """Yield a configured asynchronous S3 client. + + Yields: + Any: Asynchronous S3 client bound to the configured endpoint and credentials. + """ + session = aioboto3.Session() + + async with session.client( + "s3", + endpoint_url=settings.s3_endpoint_url, + aws_access_key_id=settings.s3_access_key_id, + aws_secret_access_key=settings.s3_secret_access_key, + region_name=settings.s3_region, + config=Config( + signature_version="s3v4", + s3={"addressing_style": "path"}, + ), + ) as s3: + yield s3 diff --git a/src/ai_notes_api/storage/document_storage.py b/src/ai_notes_api/storage/document_storage.py index ca5a193..8bc330e 100644 --- a/src/ai_notes_api/storage/document_storage.py +++ b/src/ai_notes_api/storage/document_storage.py @@ -1,15 +1,14 @@ """Document storage module. This module provides a storage helper for uploading, downloading, and deleting -documents in MinIO object storage. +documents in S3 object storage. """ -from datetime import timedelta -from io import BytesIO +from typing import Any from uuid import UUID +from botocore.exceptions import ClientError from loguru import logger -from minio import Minio from ai_notes_api.core import settings @@ -18,23 +17,25 @@ class DocumentStorage: """Object storage helper for documents. Args: - client (Minio): MinIO client used to perform object storage operations. + client (Any): Asynchronous S3 client used to perform object storage operations. """ - def __init__(self, client: Minio) -> None: + def __init__(self, client: Any) -> None: """Initialize the document storage helper. Args: - client (Minio): MinIO client used to perform object storage + client (Any): Asynchronous S3 client used to perform object storage operations. """ self.client = client - self.bucket = settings.minio_bucket_name + self.bucket = settings.s3_bucket_name - def ensure_bucket(self) -> None: + async def ensure_bucket(self) -> None: """Create the storage bucket if it does not already exist.""" - if not self.client.bucket_exists(self.bucket): - self.client.make_bucket(self.bucket) + try: + await self.client.head_bucket(Bucket=self.bucket) + except ClientError: + await self.client.create_bucket(Bucket=self.bucket) logger.info("Storage bucket created: bucket={}", self.bucket) def build_object_name( @@ -55,7 +56,7 @@ def build_object_name( """ return f"users/{user_id}/documents/{document_id}/original/{filename}" - def upload_file( + async def upload_file( self, user_id: UUID, document_id: UUID, @@ -75,7 +76,7 @@ def upload_file( Returns: str: Object name under which the document was stored. """ - self.ensure_bucket() + await self.ensure_bucket() object_name = self.build_object_name( user_id=user_id, @@ -83,12 +84,11 @@ def upload_file( filename=filename, ) - self.client.put_object( - bucket_name=self.bucket, - object_name=object_name, - data=BytesIO(data), - length=len(data), - content_type=content_type, + await self.client.put_object( + Bucket=self.bucket, + Key=object_name, + Body=data, + ContentType=content_type, ) logger.info( @@ -99,7 +99,7 @@ def upload_file( return object_name - def download_file(self, object_name: str) -> bytes: + async def download_file(self, object_name: str) -> bytes: """Download a document from object storage. Args: @@ -108,19 +108,23 @@ def download_file(self, object_name: str) -> bytes: Returns: bytes: Raw document content. """ - response = self.client.get_object(self.bucket, object_name) + response = await self.client.get_object( + Bucket=self.bucket, + Key=object_name, + ) + + body = response["Body"] try: - data = response.read() + data: bytes = await body.read() finally: - response.close() - response.release_conn() + body.close() logger.debug("Document downloaded: object_name={}", object_name) return data - def get_presigned_download_url( + async def get_presigned_download_url( self, object_name: str, expires_in_seconds: int | None = None, @@ -136,12 +140,12 @@ def get_presigned_download_url( str: Presigned URL used to download the document. """ if expires_in_seconds is None: - expires_in_seconds = settings.minio_presigned_url_expire_seconds + expires_in_seconds = settings.s3_presigned_url_expire_seconds - url = self.client.presigned_get_object( - bucket_name=self.bucket, - object_name=object_name, - expires=timedelta(seconds=expires_in_seconds), + url: str = await self.client.generate_presigned_url( + "get_object", + Params={"Bucket": self.bucket, "Key": object_name}, + ExpiresIn=expires_in_seconds, ) logger.debug( @@ -152,12 +156,12 @@ def get_presigned_download_url( return url - def delete_file(self, object_name: str) -> None: + async def delete_file(self, object_name: str) -> None: """Delete a document from object storage. Args: object_name (str): Object name within the storage bucket. """ - self.client.remove_object(self.bucket, object_name) + await self.client.delete_object(Bucket=self.bucket, Key=object_name) logger.info("Document deleted: object_name={}", object_name) From 5f563f42837d39b7fc3d12b09b768b43dca26ed7 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:22:55 +0300 Subject: [PATCH 41/81] feat(model): add document_processing_job model --- src/ai_notes_api/db/models/__init__.py | 6 ++ src/ai_notes_api/db/models/document.py | 8 ++ .../db/models/document_processing_job.py | 100 ++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 src/ai_notes_api/db/models/document_processing_job.py diff --git a/src/ai_notes_api/db/models/__init__.py b/src/ai_notes_api/db/models/__init__.py index 29b423f..4a7f132 100644 --- a/src/ai_notes_api/db/models/__init__.py +++ b/src/ai_notes_api/db/models/__init__.py @@ -9,6 +9,10 @@ from ai_notes_api.db.models.datetime import SoftDeleteMixin, TimestampMixin from ai_notes_api.db.models.document import Document, DocumentStatus from ai_notes_api.db.models.document_chunk import DocumentChunk +from ai_notes_api.db.models.document_processing_job import ( + DocumentProcessingJob, + DocumentProcessingJobStatus, +) from ai_notes_api.db.models.generation_job import GenerationJob, GenerationJobStatus from ai_notes_api.db.models.message import Message, MessageRole from ai_notes_api.db.models.note import ModelSource, Note @@ -36,4 +40,6 @@ "RagQuery", "RagQueryStatus", "RagQuerySource", + "DocumentProcessingJob", + "DocumentProcessingJobStatus", ] diff --git a/src/ai_notes_api/db/models/document.py b/src/ai_notes_api/db/models/document.py index 1c5d8eb..a900560 100644 --- a/src/ai_notes_api/db/models/document.py +++ b/src/ai_notes_api/db/models/document.py @@ -19,6 +19,7 @@ if TYPE_CHECKING: from ai_notes_api.db.models.chat_session import ChatSession from ai_notes_api.db.models.document_chunk import DocumentChunk + from ai_notes_api.db.models.document_processing_job import DocumentProcessingJob from ai_notes_api.db.models.rag_query_source import RagQuerySource from ai_notes_api.db.models.user import User @@ -67,6 +68,8 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): document. rag_query_sources (Mapped[list[RagQuerySource]]): RAG query sources that reference the document. + processing_jobs (Mapped[list[DocumentProcessingJob]]): Processing jobs + that belong to the document. """ __tablename__ = "documents" @@ -161,3 +164,8 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): back_populates="document", cascade="all, delete-orphan", ) + + processing_jobs: Mapped[list["DocumentProcessingJob"]] = relationship( + back_populates="document", + cascade="all, delete-orphan", + ) diff --git a/src/ai_notes_api/db/models/document_processing_job.py b/src/ai_notes_api/db/models/document_processing_job.py new file mode 100644 index 0000000..2ae2ea1 --- /dev/null +++ b/src/ai_notes_api/db/models/document_processing_job.py @@ -0,0 +1,100 @@ +"""Document processing job database model module. + +This module defines the SQLAlchemy ORM model for document processing jobs and +the enum used to track processing job status. +""" + +from datetime import datetime +from enum import StrEnum +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from sqlalchemy import DateTime, ForeignKey, Text, Uuid +from sqlalchemy import Enum as SqlEnum +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from ai_notes_api.db.models.base import Base +from ai_notes_api.db.models.datetime import TimestampMixin + +if TYPE_CHECKING: + from ai_notes_api.db.models.document import Document + + +class DocumentProcessingJobStatus(StrEnum): + """Status of a document processing job. + + Attributes: + QUEUED (str): Processing job is waiting to be processed. + RUNNING (str): Processing job is currently being processed. + COMPLETED (str): Processing job completed successfully. + FAILED (str): Processing job failed. + """ + + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + + +class DocumentProcessingJob(Base, TimestampMixin): + """SQLAlchemy ORM model representing a document processing job. + + Attributes: + id (Mapped[UUID]): Unique processing job identifier. + document_id (Mapped[UUID]): Identifier of the document the processing job + belongs to. + document (Mapped[Document]): Document the processing job belongs to. + status (Mapped[DocumentProcessingJobStatus]): Current processing job status. + started_at (Mapped[datetime | None]): Date and time when processing started. + finished_at (Mapped[datetime | None]): Date and time when processing finished. + error (Mapped[str | None]): Optional error message if processing failed. + """ + + __tablename__ = "document_processing_jobs" + + id: Mapped[UUID] = mapped_column( + Uuid, + primary_key=True, + default=uuid4, + ) + + document_id: Mapped[UUID] = mapped_column( + ForeignKey( + "documents.id", + ondelete="CASCADE", + ), + nullable=False, + index=True, + ) + + document: Mapped["Document"] = relationship( + back_populates="processing_jobs", + ) + + status: Mapped[DocumentProcessingJobStatus] = mapped_column( + SqlEnum( + DocumentProcessingJobStatus, + name="document_processing_job_status", + values_callable=lambda enum_cls: [item.value for item in enum_cls], + ), + default=DocumentProcessingJobStatus.QUEUED, + nullable=False, + ) + + started_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), + default=None, + nullable=True, + ) + + finished_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), + default=None, + nullable=True, + ) + + error: Mapped[str | None] = mapped_column( + Text, + default=None, + nullable=True, + ) From bdf7d97c89c3370f69570d7498b6015cf9b5621c Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:33:37 +0300 Subject: [PATCH 42/81] feat(model): remove processed_at column --- src/ai_notes_api/db/models/document.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/ai_notes_api/db/models/document.py b/src/ai_notes_api/db/models/document.py index a900560..6928ad7 100644 --- a/src/ai_notes_api/db/models/document.py +++ b/src/ai_notes_api/db/models/document.py @@ -4,13 +4,12 @@ used to track document processing status. """ -from datetime import datetime from enum import StrEnum from typing import TYPE_CHECKING from uuid import UUID, uuid4 -from sqlalchemy import DateTime, ForeignKey, String, Text, Uuid from sqlalchemy import Enum as SqlEnum +from sqlalchemy import ForeignKey, String, Text, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship from ai_notes_api.db.models.base import Base @@ -62,8 +61,6 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): status (Mapped[DocumentStatus]): Current document processing status. error_message (Mapped[str | None]): Optional error message if document processing failed. - processed_at (Mapped[datetime | None]): Date and time when the document - finished processing. document_chunks (Mapped[list[DocumentChunk]]): Chunks that belong to the document. rag_query_sources (Mapped[list[RagQuerySource]]): RAG query sources that @@ -149,12 +146,6 @@ class Document(Base, TimestampMixin, SoftDeleteMixin): nullable=True, ) - processed_at: Mapped[datetime | None] = mapped_column( - DateTime(timezone=True), - default=None, - nullable=True, - ) - document_chunks: Mapped[list["DocumentChunk"]] = relationship( back_populates="document", cascade="all, delete-orphan", From decea20e17280ad390d99d0ae2f7cdbd1b0712ff Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:34:33 +0300 Subject: [PATCH 43/81] build(alembic): add document_processing_jobs table --- ...bda8_add_document_processing_jobs_table.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 alembic/versions/48add2a2bda8_add_document_processing_jobs_table.py diff --git a/alembic/versions/48add2a2bda8_add_document_processing_jobs_table.py b/alembic/versions/48add2a2bda8_add_document_processing_jobs_table.py new file mode 100644 index 0000000..1bcfb9d --- /dev/null +++ b/alembic/versions/48add2a2bda8_add_document_processing_jobs_table.py @@ -0,0 +1,45 @@ +"""'Add document_processing_jobs table' + +Revision ID: 48add2a2bda8 +Revises: e495a670b858 +Create Date: 2026-06-23 07:33:08.783932 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '48add2a2bda8' +down_revision: Union[str, Sequence[str], None] = 'e495a670b858' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('document_processing_jobs', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('document_id', sa.Uuid(), nullable=False), + sa.Column('status', sa.Enum('queued', 'running', 'completed', 'failed', name='document_processing_job_status'), nullable=False), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('finished_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['document_id'], ['documents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_document_processing_jobs_document_id'), 'document_processing_jobs', ['document_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_document_processing_jobs_document_id'), table_name='document_processing_jobs') + op.drop_table('document_processing_jobs') + # ### end Alembic commands ### From 3e74cd477e7ed229dca756d4dfd818d084a4ae4d Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:37:03 +0300 Subject: [PATCH 44/81] feat(repository): add document_processing_job repository --- src/ai_notes_api/repositories/__init__.py | 4 + .../repositories/document_processing_job.py | 114 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/ai_notes_api/repositories/document_processing_job.py diff --git a/src/ai_notes_api/repositories/__init__.py b/src/ai_notes_api/repositories/__init__.py index b0652a0..6564214 100644 --- a/src/ai_notes_api/repositories/__init__.py +++ b/src/ai_notes_api/repositories/__init__.py @@ -8,6 +8,9 @@ from ai_notes_api.repositories.chat_session import ChatSessionRepository from ai_notes_api.repositories.document import DocumentRepository from ai_notes_api.repositories.document_chunk import DocumentChunkRepository +from ai_notes_api.repositories.document_processing_job import ( + DocumentProcessingJobRepository, +) from ai_notes_api.repositories.filters import ( ChatSessionListFilters, GenerationJobListFilters, @@ -37,4 +40,5 @@ "DocumentChunkRepository", "RagQueryRepository", "RagQuerySourceRepository", + "DocumentProcessingJobRepository", ] diff --git a/src/ai_notes_api/repositories/document_processing_job.py b/src/ai_notes_api/repositories/document_processing_job.py new file mode 100644 index 0000000..9c6d0fe --- /dev/null +++ b/src/ai_notes_api/repositories/document_processing_job.py @@ -0,0 +1,114 @@ +"""Document processing job repository module. + +This module provides a repository for creating, reading, and updating document +processing jobs in the database. +""" + +from uuid import UUID + +from loguru import logger +from sqlalchemy import select + +from ai_notes_api.db.models import DocumentProcessingJob +from ai_notes_api.repositories.base import BaseRepository + + +class DocumentProcessingJobRepository(BaseRepository): + """Repository for document processing job database operations.""" + + async def create( + self, + processing_job: DocumentProcessingJob, + ) -> DocumentProcessingJob: + """Create a document processing job in the database. + + Args: + processing_job (DocumentProcessingJob): Processing job instance to + persist. + + Returns: + DocumentProcessingJob: Persisted processing job with refreshed + database-generated fields. + """ + self.session.add(processing_job) + + await self.session.flush() + await self.session.refresh(processing_job) + + logger.info("Document processing job created: id={}", processing_job.id) + + return processing_job + + async def get_by_id(self, job_id: UUID) -> DocumentProcessingJob | None: + """Return a document processing job by its identifier. + + Args: + job_id (UUID): Unique processing job identifier. + + Returns: + DocumentProcessingJob | None: Matching processing job if found; + otherwise, None. + """ + stmt = select(DocumentProcessingJob).where( + DocumentProcessingJob.id == job_id, + ) + + result = await self.session.execute(stmt) + processing_job = result.scalar_one_or_none() + + if processing_job is None: + logger.debug("Document processing job not found: id={}", job_id) + else: + logger.debug("Document processing job found: id={}", job_id) + + return processing_job + + async def get_list_for_document( + self, + document_id: UUID, + ) -> list[DocumentProcessingJob]: + """Return processing jobs for a document. + + Args: + document_id (UUID): Unique document identifier. + + Returns: + list[DocumentProcessingJob]: List of matching processing jobs ordered + by creation date in descending order. + """ + stmt = ( + select(DocumentProcessingJob) + .where(DocumentProcessingJob.document_id == document_id) + .order_by(DocumentProcessingJob.created_at.desc()) + ) + + result = await self.session.execute(stmt) + processing_jobs = list(result.scalars().all()) + + logger.debug( + "Document processing jobs list fetched: count={}, document_id={}", + len(processing_jobs), + document_id, + ) + + return processing_jobs + + async def update( + self, + processing_job: DocumentProcessingJob, + ) -> DocumentProcessingJob: + """Update an existing document processing job in the database. + + Args: + processing_job (DocumentProcessingJob): Processing job instance with + updated field values. + + Returns: + DocumentProcessingJob: Updated and refreshed processing job instance. + """ + await self.session.flush() + await self.session.refresh(processing_job) + + logger.info("Document processing job updated: id={}", processing_job.id) + + return processing_job From d9d48e1d6915b82ffd0117b41a008629c224c9e1 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:37:49 +0300 Subject: [PATCH 45/81] feat(schemas): add document related schemas --- src/ai_notes_api/schemas/__init__.py | 2 + src/ai_notes_api/schemas/document.py | 3 -- .../schemas/document_processing_job.py | 37 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/ai_notes_api/schemas/document_processing_job.py diff --git a/src/ai_notes_api/schemas/__init__.py b/src/ai_notes_api/schemas/__init__.py index 6a22c8a..3de02df 100644 --- a/src/ai_notes_api/schemas/__init__.py +++ b/src/ai_notes_api/schemas/__init__.py @@ -21,6 +21,7 @@ DocumentRead, DocumentUploadResponse, ) +from ai_notes_api.schemas.document_processing_job import DocumentProcessingJobRead from ai_notes_api.schemas.error import ErrorResponseSchema from ai_notes_api.schemas.generation_job import ( GenerationJobCreateSchema, @@ -85,6 +86,7 @@ "DocumentDeleteResponse", "DocumentDownloadUrlResponse", "DocumentChunkRead", + "DocumentProcessingJobRead", "RagQueryRequest", "RagSourceRead", "RagQueryResponse", diff --git a/src/ai_notes_api/schemas/document.py b/src/ai_notes_api/schemas/document.py index d07b3aa..bee5ec2 100644 --- a/src/ai_notes_api/schemas/document.py +++ b/src/ai_notes_api/schemas/document.py @@ -27,8 +27,6 @@ class DocumentRead(BaseModel): processing failed. created_at (datetime): Date and time when the document was created. updated_at (datetime): Date and time when the document was last updated. - processed_at (datetime | None): Optional date and time when the document - finished processing. """ model_config = ConfigDict(from_attributes=True, populate_by_name=True) @@ -46,7 +44,6 @@ class DocumentRead(BaseModel): created_at: datetime updated_at: datetime - processed_at: datetime | None = None class DocumentListResponse(BaseModel): diff --git a/src/ai_notes_api/schemas/document_processing_job.py b/src/ai_notes_api/schemas/document_processing_job.py new file mode 100644 index 0000000..d7015a1 --- /dev/null +++ b/src/ai_notes_api/schemas/document_processing_job.py @@ -0,0 +1,37 @@ +"""Document processing job schemas module. + +This module defines Pydantic schemas used for document processing job API +responses. +""" + +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict + +from ai_notes_api.db.models import DocumentProcessingJobStatus + + +class DocumentProcessingJobRead(BaseModel): + """Schema for returning document processing job data. + + Attributes: + id (UUID): Unique processing job identifier. + document_id (UUID): Unique document identifier. + status (DocumentProcessingJobStatus): Current processing job status. + created_at (datetime): Date and time when the processing job was created. + started_at (datetime | None): Date and time when processing started. + finished_at (datetime | None): Date and time when processing finished. + error (str | None): Optional error message if processing failed. + """ + + model_config = ConfigDict(from_attributes=True) + + id: UUID + document_id: UUID + status: DocumentProcessingJobStatus + + created_at: datetime + started_at: datetime | None = None + finished_at: datetime | None = None + error: str | None = None From 616f5e4176bb95ce25b2cc975a0fd122c8207ceb Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:38:12 +0300 Subject: [PATCH 46/81] feat(service): add document service --- src/ai_notes_api/services/__init__.py | 2 + src/ai_notes_api/services/document.py | 161 ++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 src/ai_notes_api/services/document.py diff --git a/src/ai_notes_api/services/__init__.py b/src/ai_notes_api/services/__init__.py index 8a0ab67..3ac8aee 100644 --- a/src/ai_notes_api/services/__init__.py +++ b/src/ai_notes_api/services/__init__.py @@ -6,6 +6,7 @@ from ai_notes_api.services.auth import AuthService from ai_notes_api.services.chat_memory import ChatMemoryService from ai_notes_api.services.chat_session import ChatSessionService +from ai_notes_api.services.document import DocumentService from ai_notes_api.services.generation_job import JobService from ai_notes_api.services.llm_service import LLMService from ai_notes_api.services.message import MessageService @@ -19,4 +20,5 @@ "NoteService", "LLMService", "ChatMemoryService", + "DocumentService", ] diff --git a/src/ai_notes_api/services/document.py b/src/ai_notes_api/services/document.py new file mode 100644 index 0000000..b7a866e --- /dev/null +++ b/src/ai_notes_api/services/document.py @@ -0,0 +1,161 @@ +"""Document service module. + +This module provides business logic for working with documents. +""" + +import hashlib +from uuid import UUID, uuid4 + +from fastapi import UploadFile + +from ai_notes_api.db.models import Document, DocumentStatus +from ai_notes_api.exceptions import DocumentNotFoundError +from ai_notes_api.repositories import DocumentRepository +from ai_notes_api.storage import DocumentStorage + +DEFAULT_FILENAME = "document" +DEFAULT_CONTENT_TYPE = "application/octet-stream" + + +class DocumentService: + """Service for document-related business operations. + + Args: + repository (DocumentRepository): Repository used to perform document + database operations. + storage (DocumentStorage): Object storage helper used to manage document + files. + """ + + def __init__( + self, + repository: DocumentRepository, + storage: DocumentStorage, + ) -> None: + """Initialize the document service. + + Args: + repository (DocumentRepository): Document repository used by the service. + storage (DocumentStorage): Object storage helper used by the service. + """ + self.documents = repository + self.storage = storage + + async def create_document( + self, + user_id: UUID, + chat_session_id: UUID, + file: UploadFile, + ) -> Document: + """Upload a file and create a document for a chat session. + + Reads the uploaded file, stores it in object storage, and persists a + document record in the ``UPLOADED`` status. + + Args: + user_id (UUID): Unique identifier of the user uploading the document. + chat_session_id (UUID): Unique chat session identifier. + file (UploadFile): Uploaded file to store as a document. + + Returns: + Document: Created document. + """ + data = await file.read() + + document_id = uuid4() + filename = file.filename or DEFAULT_FILENAME + content_type = file.content_type or DEFAULT_CONTENT_TYPE + checksum = hashlib.sha256(data).hexdigest() + + object_name = await self.storage.upload_file( + user_id=user_id, + document_id=document_id, + filename=filename, + data=data, + content_type=content_type, + ) + + document = Document( + id=document_id, + user_id=user_id, + session_id=chat_session_id, + filename=filename, + content_type=content_type, + file_size=len(data), + checksum_sha256=checksum, + storage_bucket=self.storage.bucket, + storage_object_name=object_name, + status=DocumentStatus.UPLOADED, + ) + + return await self.documents.create(document) + + async def list_chat_documents( + self, + user_id: UUID, + chat_session_id: UUID, + ) -> list[Document]: + """Return a user's documents for a chat session. + + Args: + user_id (UUID): Unique identifier of the user who owns the documents. + chat_session_id (UUID): Unique chat session identifier. + + Returns: + list[Document]: List of the user's documents in the chat session. + """ + return await self.documents.get_list_for_session(user_id, chat_session_id) + + async def get_chat_document( + self, + user_id: UUID, + chat_session_id: UUID, + document_id: UUID, + ) -> Document: + """Return a user's document from a chat session by its identifier. + + Args: + user_id (UUID): Unique identifier of the user who owns the document. + chat_session_id (UUID): Unique chat session identifier. + document_id (UUID): Unique document identifier. + + Returns: + Document: Matching document. + + Raises: + DocumentNotFoundError: If no accessible document exists in the chat session. + """ + document = await self.documents.get_by_id_for_user(user_id, document_id) + + if document is None or document.session_id != chat_session_id: + raise DocumentNotFoundError() + + return document + + async def delete_document( + self, + user_id: UUID, + chat_session_id: UUID, + document_id: UUID, + ) -> None: + """Delete a user's document from a chat session. + + Soft-deletes the document and its chunks, then removes the stored file + from object storage. + + Args: + user_id (UUID): Unique identifier of the user who owns the document. + chat_session_id (UUID): Unique chat session identifier. + document_id (UUID): Unique document identifier to delete. + + Raises: + DocumentNotFoundError: If no accessible document exists in the chat session. + """ + document = await self.get_chat_document( + user_id, + chat_session_id, + document_id, + ) + + await self.documents.soft_delete(document) + await self.storage.delete_file(document.storage_object_name) From b1eda7269619e4e0bd000ba67e40b07b0a031eaa Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:38:45 +0300 Subject: [PATCH 47/81] build(alembic): update migration --- ...e8_add_processed_at_column_in_documents.py | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py diff --git a/alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py b/alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py deleted file mode 100644 index c37bf08..0000000 --- a/alembic/versions/89a8d53bd0e8_add_processed_at_column_in_documents.py +++ /dev/null @@ -1,32 +0,0 @@ -"""'Add processed_at column in documents' - -Revision ID: 89a8d53bd0e8 -Revises: e495a670b858 -Create Date: 2026-06-23 06:25:52.857746 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '89a8d53bd0e8' -down_revision: Union[str, Sequence[str], None] = 'e495a670b858' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('documents', sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('documents', 'processed_at') - # ### end Alembic commands ### From 5d6b38b0ac9626e828abc4cadf5d888a27b008b8 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:39:11 +0300 Subject: [PATCH 48/81] feat(exceptions): add document related exceptions --- src/ai_notes_api/exceptions/__init__.py | 6 ++++++ src/ai_notes_api/exceptions/document.py | 17 +++++++++++++++++ .../exceptions/document_processing_job.py | 17 +++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/ai_notes_api/exceptions/document.py create mode 100644 src/ai_notes_api/exceptions/document_processing_job.py diff --git a/src/ai_notes_api/exceptions/__init__.py b/src/ai_notes_api/exceptions/__init__.py index 2169305..e369d1e 100644 --- a/src/ai_notes_api/exceptions/__init__.py +++ b/src/ai_notes_api/exceptions/__init__.py @@ -11,6 +11,10 @@ MemoryInProgressError, ) from ai_notes_api.exceptions.chat_session import ChatSessionNotFoundError +from ai_notes_api.exceptions.document import DocumentNotFoundError +from ai_notes_api.exceptions.document_processing_job import ( + DocumentProcessingJobNotFoundError, +) from ai_notes_api.exceptions.generation_job import ( GenerationInProgressError, GenerationNotFoundError, @@ -41,4 +45,6 @@ "ChatMemoryNotFoundError", "MemoryInProgressError", "ChatMemoryDependenciesNotConfiguredError", + "DocumentNotFoundError", + "DocumentProcessingJobNotFoundError", ] diff --git a/src/ai_notes_api/exceptions/document.py b/src/ai_notes_api/exceptions/document.py new file mode 100644 index 0000000..6801b73 --- /dev/null +++ b/src/ai_notes_api/exceptions/document.py @@ -0,0 +1,17 @@ +"""Document exception module. + +This module defines application exceptions related to documents. +""" + +from ai_notes_api.exceptions import AppException + + +class DocumentNotFoundError(AppException): + """Exception raised when a document is not found.""" + + status_code: int = 404 + code: str = "DOCUMENT_NOT_FOUND" + + def __init__(self) -> None: + """Initialize the document not found exception.""" + super().__init__("Document not found") diff --git a/src/ai_notes_api/exceptions/document_processing_job.py b/src/ai_notes_api/exceptions/document_processing_job.py new file mode 100644 index 0000000..89a2cb2 --- /dev/null +++ b/src/ai_notes_api/exceptions/document_processing_job.py @@ -0,0 +1,17 @@ +"""Document processing job exception module. + +This module defines application exceptions related to document processing jobs. +""" + +from ai_notes_api.exceptions import AppException + + +class DocumentProcessingJobNotFoundError(AppException): + """Exception raised when a document processing job is not found.""" + + status_code: int = 404 + code: str = "DOCUMENT_PROCESSING_JOB_NOT_FOUND" + + def __init__(self) -> None: + """Initialize the document processing job not found exception.""" + super().__init__("Document processing job not found") From 4c8ec2cba9aaab3c80b1588ef89eff6a57716b55 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 07:54:20 +0300 Subject: [PATCH 49/81] feat(services): connect document with processing_job --- src/ai_notes_api/services/document.py | 73 ++++++++++++++++++++------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/src/ai_notes_api/services/document.py b/src/ai_notes_api/services/document.py index b7a866e..c12c799 100644 --- a/src/ai_notes_api/services/document.py +++ b/src/ai_notes_api/services/document.py @@ -8,37 +8,59 @@ from fastapi import UploadFile -from ai_notes_api.db.models import Document, DocumentStatus +from ai_notes_api.db.models import ( + Document, + DocumentProcessingJob, + DocumentProcessingJobStatus, + DocumentStatus, +) from ai_notes_api.exceptions import DocumentNotFoundError -from ai_notes_api.repositories import DocumentRepository +from ai_notes_api.repositories import ( + DocumentProcessingJobRepository, + DocumentRepository, +) +from ai_notes_api.services.chat_session import ChatSessionService from ai_notes_api.storage import DocumentStorage - -DEFAULT_FILENAME = "document" -DEFAULT_CONTENT_TYPE = "application/octet-stream" +from ai_notes_api.workers.tasks.processing import run_document_processing_job class DocumentService: """Service for document-related business operations. Args: - repository (DocumentRepository): Repository used to perform document - database operations. - storage (DocumentStorage): Object storage helper used to manage document - files. + document_repository (DocumentRepository): Repository used to perform + document database operations. + processing_repository (DocumentProcessingJobRepository): Repository used + to create document processing jobs. + session_service (ChatSessionService): Chat session service used to + validate chat session access. + storage (DocumentStorage): Object storage helper used to manage document files. """ + DEFAULT_FILENAME = "document" + DEFAULT_CONTENT_TYPE = "application/octet-stream" + def __init__( self, - repository: DocumentRepository, + document_repository: DocumentRepository, + processing_repository: DocumentProcessingJobRepository, + session_service: ChatSessionService, storage: DocumentStorage, ) -> None: """Initialize the document service. Args: - repository (DocumentRepository): Document repository used by the service. + document_repository (DocumentRepository): Document repository used by + the service. + processing_repository (DocumentProcessingJobRepository): Document + processing job repository used by the service. + session_service (ChatSessionService): Chat session service used by the + service. storage (DocumentStorage): Object storage helper used by the service. """ - self.documents = repository + self.documents = document_repository + self.sessions = session_service + self.processing = processing_repository self.storage = storage async def create_document( @@ -49,8 +71,9 @@ async def create_document( ) -> Document: """Upload a file and create a document for a chat session. - Reads the uploaded file, stores it in object storage, and persists a - document record in the ``UPLOADED`` status. + Reads the uploaded file, stores it in object storage, persists a + document record in the ``UPLOADED`` status, and enqueues a processing + job for it. Args: user_id (UUID): Unique identifier of the user uploading the document. @@ -59,12 +82,17 @@ async def create_document( Returns: Document: Created document. + + Raises: + ChatSessionNotFoundError: If no accessible chat session exists. """ + await self.sessions.ensure_session_owner(user_id, chat_session_id) + data = await file.read() document_id = uuid4() - filename = file.filename or DEFAULT_FILENAME - content_type = file.content_type or DEFAULT_CONTENT_TYPE + filename = file.filename or self.DEFAULT_FILENAME + content_type = file.content_type or self.DEFAULT_CONTENT_TYPE checksum = hashlib.sha256(data).hexdigest() object_name = await self.storage.upload_file( @@ -88,7 +116,18 @@ async def create_document( status=DocumentStatus.UPLOADED, ) - return await self.documents.create(document) + document = await self.documents.create(document) + + processing_job = await self.processing.create( + DocumentProcessingJob( + document_id=document_id, + status=DocumentProcessingJobStatus.QUEUED, + ) + ) + + run_document_processing_job.delay(str(processing_job.id)) + + return document async def list_chat_documents( self, From 6d536500afe0f92cf1f75256ba7781de7a3b8a98 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 08:07:21 +0300 Subject: [PATCH 50/81] test(repository): add document processing job tests --- ...test_document_processing_job_repository.py | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 tests/repositories/test_document_processing_job_repository.py diff --git a/tests/repositories/test_document_processing_job_repository.py b/tests/repositories/test_document_processing_job_repository.py new file mode 100644 index 0000000..eb470e1 --- /dev/null +++ b/tests/repositories/test_document_processing_job_repository.py @@ -0,0 +1,219 @@ +"""Tests for document processing job repository.""" + +from datetime import UTC, datetime, timedelta +from uuid import UUID, uuid4 + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from ai_notes_api.db.models import ( + ChatSession, + Document, + DocumentProcessingJob, + DocumentProcessingJobStatus, + DocumentStatus, + User, +) +from ai_notes_api.repositories.document_processing_job import ( + DocumentProcessingJobRepository, +) + + +@pytest_asyncio.fixture +async def test_user(async_session: AsyncSession) -> User: + """Create a test user.""" + user = User( + email="test-user@example.com", + username="test_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + async_session.add(user) + await async_session.flush() + await async_session.refresh(user) + + return user + + +async def create_document( + async_session: AsyncSession, + *, + user_id: UUID, +) -> Document: + """Persist a chat session and document for processing job repository tests. + + Args: + async_session (AsyncSession): Database session used to persist the rows. + user_id (UUID): Identifier of the user who owns the rows. + + Returns: + Document: Persisted document instance. + """ + chat_session = ChatSession(user_id=user_id, title="Test chat session") + + async_session.add(chat_session) + await async_session.flush() + + document = Document( + user_id=user_id, + session_id=chat_session.id, + filename="test.pdf", + content_type="application/pdf", + file_size=1024, + checksum_sha256="checksum", + storage_bucket="documents", + storage_object_name="object", + status=DocumentStatus.UPLOADED, + ) + + async_session.add(document) + await async_session.flush() + await async_session.refresh(document) + + return document + + +def create_job( + *, + document_id: UUID, + status: DocumentProcessingJobStatus = DocumentProcessingJobStatus.QUEUED, + created_at: datetime | None = None, +) -> DocumentProcessingJob: + """Create a processing job instance for repository tests. + + Args: + document_id (UUID): Identifier of the document the job belongs to. + status (DocumentProcessingJobStatus): Processing job status. + created_at (datetime | None): Optional explicit creation timestamp used to + control processing job ordering in tests. + + Returns: + DocumentProcessingJob: Processing job model instance. + """ + processing_job = DocumentProcessingJob( + document_id=document_id, + status=status, + ) + + if created_at is not None: + processing_job.created_at = created_at + + return processing_job + + +@pytest.mark.asyncio +async def test_create_job_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful processing job creation.""" + repository = DocumentProcessingJobRepository(session=async_session) + document = await create_document(async_session, user_id=test_user.id) + + job = await repository.create(create_job(document_id=document.id)) + + assert job.id is not None + assert job.document_id == document.id + assert job.status == DocumentProcessingJobStatus.QUEUED + + +@pytest.mark.asyncio +async def test_get_by_id_job_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful processing job retrieval by identifier.""" + repository = DocumentProcessingJobRepository(session=async_session) + document = await create_document(async_session, user_id=test_user.id) + + created = await repository.create(create_job(document_id=document.id)) + + job = await repository.get_by_id(created.id) + + assert job is not None + assert job.id == created.id + + +@pytest.mark.asyncio +async def test_get_by_id_job_not_found(async_session: AsyncSession) -> None: + """Test that processing job retrieval by identifier returns None when missing.""" + repository = DocumentProcessingJobRepository(session=async_session) + + job = await repository.get_by_id(uuid4()) + + assert job is None + + +@pytest.mark.asyncio +async def test_get_list_for_document_orders_by_created_at_desc( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that the jobs list is ordered by creation date in descending order.""" + repository = DocumentProcessingJobRepository(session=async_session) + document = await create_document(async_session, user_id=test_user.id) + + base = datetime.now(UTC) + + await repository.create( + create_job(document_id=document.id, created_at=base), + ) + await repository.create( + create_job(document_id=document.id, created_at=base + timedelta(seconds=1)), + ) + + jobs = await repository.get_list_for_document(document.id) + + assert len(jobs) == 2 + assert jobs[0].created_at > jobs[1].created_at + + +@pytest.mark.asyncio +async def test_get_list_for_document_scoped_to_document( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test that the jobs list is scoped to the requested document.""" + repository = DocumentProcessingJobRepository(session=async_session) + document = await create_document(async_session, user_id=test_user.id) + other_document = await create_document(async_session, user_id=test_user.id) + + owned = await repository.create(create_job(document_id=document.id)) + await repository.create(create_job(document_id=other_document.id)) + + jobs = await repository.get_list_for_document(document.id) + + assert len(jobs) == 1 + assert jobs[0].id == owned.id + + +@pytest.mark.asyncio +async def test_update_job_success( + async_session: AsyncSession, + test_user: User, +) -> None: + """Test successful processing job update.""" + repository = DocumentProcessingJobRepository(session=async_session) + document = await create_document(async_session, user_id=test_user.id) + + job = await repository.create( + create_job( + document_id=document.id, + status=DocumentProcessingJobStatus.QUEUED, + ) + ) + + job.status = DocumentProcessingJobStatus.RUNNING + job.started_at = datetime.now(UTC) + + updated = await repository.update(job) + + assert updated.status == DocumentProcessingJobStatus.RUNNING + + found = await repository.get_by_id(job.id) + + assert found is not None + assert found.status == DocumentProcessingJobStatus.RUNNING From 300d0ce4f01d33105c53da03afab4331aeb0f421 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 08:10:35 +0300 Subject: [PATCH 51/81] feat(services): add unwrited document processing job service --- src/ai_notes_api/services/__init__.py | 2 + .../services/document_processing_service.py | 193 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 src/ai_notes_api/services/document_processing_service.py diff --git a/src/ai_notes_api/services/__init__.py b/src/ai_notes_api/services/__init__.py index 3ac8aee..bf6720a 100644 --- a/src/ai_notes_api/services/__init__.py +++ b/src/ai_notes_api/services/__init__.py @@ -7,6 +7,7 @@ from ai_notes_api.services.chat_memory import ChatMemoryService from ai_notes_api.services.chat_session import ChatSessionService from ai_notes_api.services.document import DocumentService +from ai_notes_api.services.document_processing_service import DocumentProcessingService from ai_notes_api.services.generation_job import JobService from ai_notes_api.services.llm_service import LLMService from ai_notes_api.services.message import MessageService @@ -21,4 +22,5 @@ "LLMService", "ChatMemoryService", "DocumentService", + "DocumentProcessingService", ] diff --git a/src/ai_notes_api/services/document_processing_service.py b/src/ai_notes_api/services/document_processing_service.py new file mode 100644 index 0000000..6055a6e --- /dev/null +++ b/src/ai_notes_api/services/document_processing_service.py @@ -0,0 +1,193 @@ +"""Document processing service module. + +This module provides the business logic that turns an uploaded document into +embedded chunks: downloading the source file from object storage, extracting and +splitting its text, generating embeddings, persisting the resulting chunks, and +updating the document status accordingly. +""" + +import hashlib +from dataclasses import dataclass +from uuid import UUID + +from loguru import logger + +from ai_notes_api.core import settings +from ai_notes_api.db.models import Document, DocumentChunk, DocumentStatus +from ai_notes_api.exceptions import DocumentNotFoundError +from ai_notes_api.llm import EmbeddingClient +from ai_notes_api.repositories import DocumentChunkRepository, DocumentRepository +from ai_notes_api.storage import DocumentStorage + + +@dataclass(slots=True) +class TextChunk: + """Plain-text chunk produced while splitting a document. + + Attributes: + index (int): Position of the chunk within the document. + content (str): Text content of the chunk. + """ + + index: int + content: str + + +class DocumentProcessingService: + """Service that processes documents into embedded chunks. + + Args: + document_repository (DocumentRepository): Repository used to load and + update the source document. + chunk_repository (DocumentChunkRepository): Repository used to persist + document chunks. + storage (DocumentStorage): Object storage helper used to download the + source file from S3. + embeddings (EmbeddingClient): Client used to generate chunk embeddings. + """ + + CHUNK_SIZE = 1_000 + CHUNK_OVERLAP = 200 + ERROR_MAX_LENGTH = 10_000 + + def __init__( + self, + document_repository: DocumentRepository, + chunk_repository: DocumentChunkRepository, + storage: DocumentStorage, + embeddings: EmbeddingClient, + ) -> None: + """Initialize the document processing service. + + Args: + document_repository (DocumentRepository): Document repository used by + the service. + chunk_repository (DocumentChunkRepository): Document chunk repository + used by the service. + storage (DocumentStorage): Object storage helper used by the service. + embeddings (EmbeddingClient): Embedding client used by the service. + """ + self.documents = document_repository + self.chunks = chunk_repository + self.storage = storage + self.embeddings = embeddings + + async def process_document(self, document_id: UUID) -> Document: + """Process a document into embedded chunks. + + Downloads the source file from object storage, extracts and splits its + text, generates embeddings, persists the resulting chunks, and marks the + document as ``READY``. If any step fails, the document is marked + ``FAILED`` and the original error is re-raised. + + Args: + document_id (UUID): Unique identifier of the document to process. + + Returns: + Document: Processed document in its terminal status. + + Raises: + DocumentNotFoundError: If no document with the given identifier exists. + """ + document = await self.documents.get_by_id(document_id) + + if document is None: + raise DocumentNotFoundError() + + try: + logger.info("Document processing started: id={}", document_id) + + document.status = DocumentStatus.PROCESSING + document = await self.documents.update(document) + + data = await self.storage.download_file(document.storage_object_name) + + text = await self._extract_text(data, document.content_type) + text_chunks = self._chunk_text(text) + + embeddings = await self.embeddings.create_embedding( + [chunk.content for chunk in text_chunks] + ) + + chunks = [ + DocumentChunk( + user_id=document.user_id, + session_id=document.session_id, + document_id=document.id, + chunk_index=text_chunk.index, + content=text_chunk.content, + content_hash=hashlib.sha256( + text_chunk.content.encode() + ).hexdigest(), + embedding=embedding, + embedding_model=settings.open_ai_embedding_model, + ) + for text_chunk, embedding in zip(text_chunks, embeddings, strict=True) + ] + + await self.chunks.create_many(chunks) + + document = await self._mark_ready(document) + except Exception as exc: + logger.exception("Document processing failed: id={}", document_id) + + await self._mark_failed(document, str(exc)) + + raise + else: + logger.info("Document processing finished: id={}", document_id) + + return document + + async def _extract_text(self, data: bytes, content_type: str) -> str: + """Extract plain text from raw document content. + + Args: + data (bytes): Raw document content. + content_type (str): MIME type of the document used to select the + appropriate extraction strategy. + + Returns: + str: Extracted plain text. + """ + raise NotImplementedError + + def _chunk_text(self, text: str) -> list[TextChunk]: + """Split extracted text into overlapping chunks. + + Args: + text (str): Plain text to split. + + Returns: + list[TextChunk]: Ordered text chunks ready for embedding. + """ + raise NotImplementedError + + async def _mark_ready(self, document: Document) -> Document: + """Mark a document as successfully processed. + + Args: + document (Document): Document to mark as ready. + + Returns: + Document: Updated document in the ``READY`` status. + """ + document.status = DocumentStatus.READY + document.error_message = None + + return await self.documents.update(document) + + async def _mark_failed(self, document: Document, error: str) -> Document: + """Mark a document as failed. + + Args: + document (Document): Document to mark as failed. + error (str): Error message describing the failure. + + Returns: + Document: Updated document in the ``FAILED`` status. + """ + document.status = DocumentStatus.FAILED + document.error_message = error[: self.ERROR_MAX_LENGTH] + + return await self.documents.update(document) From 356e9251aed723898c18b079bfea9de9880e1dcf Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 08:10:53 +0300 Subject: [PATCH 52/81] feat(celery): add processing task --- src/ai_notes_api/workers/celery_app.py | 6 +- src/ai_notes_api/workers/tasks/processing.py | 138 +++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/ai_notes_api/workers/tasks/processing.py diff --git a/src/ai_notes_api/workers/celery_app.py b/src/ai_notes_api/workers/celery_app.py index 1dba49e..224d3fb 100644 --- a/src/ai_notes_api/workers/celery_app.py +++ b/src/ai_notes_api/workers/celery_app.py @@ -13,7 +13,11 @@ "ai_notes_api_worker", broker=settings.celery_broker_url, backend=settings.celery_result_backend, - include=["ai_notes_api.workers.tasks.generation"], + include=[ + "ai_notes_api.workers.tasks.generation", + "ai_notes_api.workers.tasks.memory", + "ai_notes_api.workers.tasks.processing", + ], ) diff --git a/src/ai_notes_api/workers/tasks/processing.py b/src/ai_notes_api/workers/tasks/processing.py new file mode 100644 index 0000000..0a0aaec --- /dev/null +++ b/src/ai_notes_api/workers/tasks/processing.py @@ -0,0 +1,138 @@ +"""Document processing worker tasks module. + +This module defines Celery tasks used to run queued document processing jobs. +""" + +import asyncio +from contextlib import asynccontextmanager +from datetime import UTC, datetime +from uuid import UUID + +from loguru import logger +from sqlalchemy.ext.asyncio import AsyncSession + +from ai_notes_api.db.models import DocumentProcessingJobStatus +from ai_notes_api.db.session import worker_session +from ai_notes_api.exceptions.document_processing_job import ( + DocumentProcessingJobNotFoundError, +) +from ai_notes_api.integrations import openai_client +from ai_notes_api.llm import EmbeddingClient +from ai_notes_api.repositories import ( + DocumentChunkRepository, + DocumentProcessingJobRepository, + DocumentRepository, +) +from ai_notes_api.services import DocumentProcessingService +from ai_notes_api.storage import DocumentStorage, get_s3_client +from ai_notes_api.workers.celery_app import celery_app + +ERROR_MAX_LENGTH = 10_000 + + +@celery_app.task(name="document.process") +def run_document_processing_job(job_id: str) -> None: + """Run a queued document processing job. + + Args: + job_id (str): Unique document processing job identifier. + """ + asyncio.run(_run_document_processing_job(UUID(job_id))) + + +async def _run_document_processing_job(job_id: UUID) -> None: + """Run a queued document processing job asynchronously. + + Args: + job_id (UUID): Unique document processing job identifier. + + Raises: + DocumentProcessingJobNotFoundError: If no document processing job with + the given identifier exists. + """ + embeddings = EmbeddingClient(openai_client) + + async with ( + worker_session() as session, + asynccontextmanager(get_s3_client)() as s3_client, + ): + processing_job_repository = DocumentProcessingJobRepository(session) + document_repository = DocumentRepository(session) + chunk_repository = DocumentChunkRepository(session) + + storage = DocumentStorage(s3_client) + + processing_job = await processing_job_repository.get_by_id(job_id) + + if processing_job is None: + raise DocumentProcessingJobNotFoundError() + + document_processing = DocumentProcessingService( + document_repository=document_repository, + chunk_repository=chunk_repository, + storage=storage, + embeddings=embeddings, + ) + + try: + logger.info("Document processing job started: id={}", job_id) + + processing_job.status = DocumentProcessingJobStatus.RUNNING + processing_job.started_at = datetime.now(UTC) + processing_job = await processing_job_repository.update(processing_job) + + document_processing.process_document(processing_job.document_id) + + processing_job.status = DocumentProcessingJobStatus.COMPLETED + processing_job.finished_at = datetime.now(UTC) + await processing_job_repository.update(processing_job) + + await session.commit() + + logger.info("Document processing job finished: id={}", job_id) + + except Exception as exc: + await session.rollback() + + logger.exception("Document processing job failed: id={}", job_id) + + await _mark_job_failed( + session=session, + job_repository=processing_job_repository, + job_id=job_id, + error=str(exc), + ) + + raise + + +async def _mark_job_failed( + session: AsyncSession, + job_repository: DocumentProcessingJobRepository, + job_id: UUID, + error: str, +) -> None: + """Mark a document processing job as failed. + + This is invoked after the job transaction has been rolled back, so it runs + in a fresh transaction to persist the failure state. + + Args: + session (AsyncSession): Database session used to commit the failure state. + job_repository (DocumentProcessingJobRepository): Repository used to + update the processing job. + job_id (UUID): Unique document processing job identifier. + error (str): Error message describing the failure. + """ + processing_job = await job_repository.get_by_id(job_id) + + if processing_job is None: + return + + processing_job.status = DocumentProcessingJobStatus.FAILED + processing_job.error = error[:ERROR_MAX_LENGTH] + processing_job.finished_at = datetime.now(UTC) + + await job_repository.update(processing_job) + + await session.commit() From be7e0c26d7c8306644617a34c6aed0142a79c4b7 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Tue, 23 Jun 2026 08:11:15 +0300 Subject: [PATCH 53/81] docs(readme): update erd --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4ccda24..a7e2468 100755 --- a/README.md +++ b/README.md @@ -266,7 +266,6 @@ erDiagram string storage_object_name enum status "document_status" text error_message "null" - datetime processed_at "null" datetime created_at datetime updated_at datetime deleted_at "null" From f915903cc95c353c88a73cfb6cd6b2bd7110a672 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Wed, 24 Jun 2026 03:04:05 +0300 Subject: [PATCH 54/81] feat(processing): add document text chunker --- pyproject.toml | 1 + src/ai_notes_api/core/config.py | 4 + src/ai_notes_api/exceptions/__init__.py | 8 +- .../exceptions/document_processing.py | 50 ++++++++ .../exceptions/document_processing_job.py | 17 --- src/ai_notes_api/services/__init__.py | 2 +- ...sing_service.py => document_processing.py} | 117 +++++++++++------- uv.lock | 116 ++++++++++++++++- 8 files changed, 253 insertions(+), 62 deletions(-) create mode 100644 src/ai_notes_api/exceptions/document_processing.py delete mode 100644 src/ai_notes_api/exceptions/document_processing_job.py rename src/ai_notes_api/services/{document_processing_service.py => document_processing.py} (67%) diff --git a/pyproject.toml b/pyproject.toml index 8023808..6caed2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "redis>=8.0.0", "pgvector>=0.4.2", "aioboto3>=15.5.0", + "tiktoken>=0.13.0", ] [dependency-groups] diff --git a/src/ai_notes_api/core/config.py b/src/ai_notes_api/core/config.py index 64bd324..b8900d0 100644 --- a/src/ai_notes_api/core/config.py +++ b/src/ai_notes_api/core/config.py @@ -29,6 +29,8 @@ class Settings(BaseSettings): open_ai_embedding_model (str): OpenAI embedding model name. open_ai_api_url (str | None): Optional custom OpenAI-compatible API URL. open_ai_max_output_tokens (int): Maximum number of output tokens. + tiktoken_encoding_name (str): Name of the tiktoken encoding used to + tokenize text when splitting documents into chunks. llm_context_messages_limit (int): Maximum number of context messages sent to the LLM. celery_broker_url (str): Celery broker URL. @@ -65,6 +67,8 @@ class Settings(BaseSettings): open_ai_api_url: str | None = Field(default=None) open_ai_max_output_tokens: int = Field(default=700) + tiktoken_encoding_name: str = Field(default="cl100k_base") + llm_context_messages_limit: int = Field(default=20) celery_broker_url: str = Field(...) diff --git a/src/ai_notes_api/exceptions/__init__.py b/src/ai_notes_api/exceptions/__init__.py index e369d1e..28885fa 100644 --- a/src/ai_notes_api/exceptions/__init__.py +++ b/src/ai_notes_api/exceptions/__init__.py @@ -12,8 +12,11 @@ ) from ai_notes_api.exceptions.chat_session import ChatSessionNotFoundError from ai_notes_api.exceptions.document import DocumentNotFoundError -from ai_notes_api.exceptions.document_processing_job import ( +from ai_notes_api.exceptions.document_processing import ( DocumentProcessingJobNotFoundError, + InvalidChunkSizeError, + InvalidOverlapError, + OverlapGreaterThanOrEqualChunkSizeError, ) from ai_notes_api.exceptions.generation_job import ( GenerationInProgressError, @@ -47,4 +50,7 @@ "ChatMemoryDependenciesNotConfiguredError", "DocumentNotFoundError", "DocumentProcessingJobNotFoundError", + "InvalidChunkSizeError", + "InvalidOverlapError", + "OverlapGreaterThanOrEqualChunkSizeError", ] diff --git a/src/ai_notes_api/exceptions/document_processing.py b/src/ai_notes_api/exceptions/document_processing.py new file mode 100644 index 0000000..857af38 --- /dev/null +++ b/src/ai_notes_api/exceptions/document_processing.py @@ -0,0 +1,50 @@ +"""Document processing job exception module. + +This module defines application exceptions related to document processing jobs. +""" + +from ai_notes_api.exceptions import AppException + + +class DocumentProcessingJobNotFoundError(AppException): + """Exception raised when a document processing job is not found.""" + + status_code: int = 404 + code: str = "DOCUMENT_PROCESSING_JOB_NOT_FOUND" + + def __init__(self) -> None: + """Initialize the document processing job not found exception.""" + super().__init__("Document processing job not found") + + +class InvalidChunkSizeError(AppException): + """Exception raised when chunk_size is invalid.""" + + status_code: int = 400 + code: str = "INVALID_CHUNK_SIZE" + + def __init__(self) -> None: + """Initialize invalid chunk_size exception.""" + super().__init__("chunk_size должен быть > 0") + + +class InvalidOverlapError(AppException): + """Exception raised when overlap is invalid.""" + + status_code: int = 400 + code: str = "INVALID_OVERLAP" + + def __init__(self) -> None: + """Initialize invalid overlap exception.""" + super().__init__("overlap должен быть >= 0") + + +class OverlapGreaterThanOrEqualChunkSizeError(AppException): + """Exception raised when overlap is greater than or equal to chunk_size.""" + + status_code: int = 400 + code: str = "OVERLAP_GREATER_THAN_OR_EQUAL_CHUNK_SIZE" + + def __init__(self) -> None: + """Initialize overlap and chunk_size validation exception.""" + super().__init__("overlap должен быть меньше chunk_size") diff --git a/src/ai_notes_api/exceptions/document_processing_job.py b/src/ai_notes_api/exceptions/document_processing_job.py deleted file mode 100644 index 89a2cb2..0000000 --- a/src/ai_notes_api/exceptions/document_processing_job.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Document processing job exception module. - -This module defines application exceptions related to document processing jobs. -""" - -from ai_notes_api.exceptions import AppException - - -class DocumentProcessingJobNotFoundError(AppException): - """Exception raised when a document processing job is not found.""" - - status_code: int = 404 - code: str = "DOCUMENT_PROCESSING_JOB_NOT_FOUND" - - def __init__(self) -> None: - """Initialize the document processing job not found exception.""" - super().__init__("Document processing job not found") diff --git a/src/ai_notes_api/services/__init__.py b/src/ai_notes_api/services/__init__.py index bf6720a..a4fc662 100644 --- a/src/ai_notes_api/services/__init__.py +++ b/src/ai_notes_api/services/__init__.py @@ -7,7 +7,7 @@ from ai_notes_api.services.chat_memory import ChatMemoryService from ai_notes_api.services.chat_session import ChatSessionService from ai_notes_api.services.document import DocumentService -from ai_notes_api.services.document_processing_service import DocumentProcessingService +from ai_notes_api.services.document_processing import DocumentProcessingService from ai_notes_api.services.generation_job import JobService from ai_notes_api.services.llm_service import LLMService from ai_notes_api.services.message import MessageService diff --git a/src/ai_notes_api/services/document_processing_service.py b/src/ai_notes_api/services/document_processing.py similarity index 67% rename from src/ai_notes_api/services/document_processing_service.py rename to src/ai_notes_api/services/document_processing.py index 6055a6e..e36d2fa 100644 --- a/src/ai_notes_api/services/document_processing_service.py +++ b/src/ai_notes_api/services/document_processing.py @@ -7,32 +7,24 @@ """ import hashlib -from dataclasses import dataclass from uuid import UUID +import tiktoken from loguru import logger from ai_notes_api.core import settings from ai_notes_api.db.models import Document, DocumentChunk, DocumentStatus -from ai_notes_api.exceptions import DocumentNotFoundError +from ai_notes_api.exceptions import ( + DocumentNotFoundError, + InvalidChunkSizeError, + InvalidOverlapError, + OverlapGreaterThanOrEqualChunkSizeError, +) from ai_notes_api.llm import EmbeddingClient from ai_notes_api.repositories import DocumentChunkRepository, DocumentRepository from ai_notes_api.storage import DocumentStorage -@dataclass(slots=True) -class TextChunk: - """Plain-text chunk produced while splitting a document. - - Attributes: - index (int): Position of the chunk within the document. - content (str): Text content of the chunk. - """ - - index: int - content: str - - class DocumentProcessingService: """Service that processes documents into embedded chunks. @@ -75,11 +67,6 @@ def __init__( async def process_document(self, document_id: UUID) -> Document: """Process a document into embedded chunks. - Downloads the source file from object storage, extracts and splits its - text, generates embeddings, persists the resulting chunks, and marks the - document as ``READY``. If any step fails, the document is marked - ``FAILED`` and the original error is re-raised. - Args: document_id (UUID): Unique identifier of the document to process. @@ -105,35 +92,40 @@ async def process_document(self, document_id: UUID) -> Document: text = await self._extract_text(data, document.content_type) text_chunks = self._chunk_text(text) - embeddings = await self.embeddings.create_embedding( - [chunk.content for chunk in text_chunks] - ) - - chunks = [ - DocumentChunk( - user_id=document.user_id, - session_id=document.session_id, - document_id=document.id, - chunk_index=text_chunk.index, - content=text_chunk.content, - content_hash=hashlib.sha256( - text_chunk.content.encode() - ).hexdigest(), - embedding=embedding, - embedding_model=settings.open_ai_embedding_model, + embeddings = await self.embeddings.create_embedding(text_chunks) + + chunks = [] + + for chunk_index in range(len(text_chunks)): + text_chunk = text_chunks[chunk_index] + embedding = embeddings[chunk_index] + + chunk_hash = hashlib.sha256(text_chunk.encode()) + + chunks.append( + DocumentChunk( + user_id=document.user_id, + session_id=document.session_id, + document_id=document.id, + chunk_index=chunk_index, + content=text_chunk, + content_hash=(chunk_hash).hexdigest(), + embedding=embedding, + embedding_model=settings.open_ai_embedding_model, + ) ) - for text_chunk, embedding in zip(text_chunks, embeddings, strict=True) - ] await self.chunks.create_many(chunks) document = await self._mark_ready(document) + except Exception as exc: logger.exception("Document processing failed: id={}", document_id) await self._mark_failed(document, str(exc)) raise + else: logger.info("Document processing finished: id={}", document_id) @@ -152,16 +144,57 @@ async def _extract_text(self, data: bytes, content_type: str) -> str: """ raise NotImplementedError - def _chunk_text(self, text: str) -> list[TextChunk]: - """Split extracted text into overlapping chunks. + def _chunk_text( + self, + text: str, + chunk_size: int = 1000, + overlap: int = 200, + ) -> list[str]: + """Split extracted text into token-based overlapping chunks. Args: text (str): Plain text to split. + chunk_size (int): Maximum number of tokens in each chunk. + overlap (int): Number of tokens repeated between adjacent chunks. Returns: - list[TextChunk]: Ordered text chunks ready for embedding. + list[str]: Ordered non-empty text chunks ready for embedding. + + Raises: + InvalidChunkSizeError: If `chunk_size` is less than or equal to zero. + InvalidOverlapError: If `overlap` is negative. + OverlapGreaterThanOrEqualChunkSizeError: If `overlap` is greater than + or equal to `chunk_size`. """ - raise NotImplementedError + if chunk_size <= 0: + raise InvalidChunkSizeError() + + if overlap < 0: + raise InvalidOverlapError() + + if overlap >= chunk_size: + raise OverlapGreaterThanOrEqualChunkSizeError() + + encoding = tiktoken.get_encoding(settings.tiktoken_encoding_name) + + tokens = encoding.encode(text) + chunks = [] + + step = chunk_size - overlap + + for start in range(0, len(tokens), step): + end = start + chunk_size + chunk_tokens = tokens[start:end] + + chunk = encoding.decode(chunk_tokens).strip() + + if chunk: + chunks.append(chunk) + + if end >= len(tokens): + break + + return chunks async def _mark_ready(self, document: Document) -> Document: """Mark a document as successfully processed. diff --git a/uv.lock b/uv.lock index 7c23793..47737ca 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.15'", @@ -30,6 +30,7 @@ dependencies = [ { name = "redis" }, { name = "sqlalchemy" }, { name = "sse-starlette" }, + { name = "tiktoken" }, { name = "uvicorn" }, ] @@ -71,6 +72,7 @@ requires-dist = [ { name = "redis", specifier = ">=8.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.50" }, { name = "sse-starlette", specifier = ">=3.4.4" }, + { name = "tiktoken", specifier = ">=0.13.0" }, { name = "uvicorn", specifier = ">=0.48.0" }, ] @@ -2064,6 +2066,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/e3/b519734372d305bd547534a9f32e4ce9f98552af753dce72cf3483a0ff0b/redis-8.0.0-py3-none-any.whl", hash = "sha256:c938c18338585009f0bc310f4c7e4e4b4d37639356c4ac072cedf3af570c8dc7", size = 499870, upload-time = "2026-05-28T12:45:11.697Z" }, ] +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +] + [[package]] name = "requests" version = "2.34.2" @@ -2248,6 +2322,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, +] + [[package]] name = "tomli" version = "2.4.1" From 7d3f1d86686e1ae32210d7af668b1b5e5b3b6dbf Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Wed, 24 Jun 2026 03:08:26 +0300 Subject: [PATCH 55/81] build(uv): add markitdown dependency --- pyproject.toml | 1 + uv.lock | 179 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 177 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6caed2a..0438ed0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "pgvector>=0.4.2", "aioboto3>=15.5.0", "tiktoken>=0.13.0", + "markitdown>=0.1.6", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 47737ca..bb65d37 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.13" resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version == '3.14.*'", - "python_full_version < '3.14'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", ] [[package]] @@ -20,6 +23,7 @@ dependencies = [ { name = "fastapi" }, { name = "greenlet" }, { name = "loguru" }, + { name = "markitdown" }, { name = "openai" }, { name = "passlib" }, { name = "pgvector" }, @@ -62,6 +66,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.136.3" }, { name = "greenlet", specifier = ">=3.5.1" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "markitdown", specifier = ">=0.1.6" }, { name = "openai", specifier = ">=2.41.1" }, { name = "passlib", specifier = ">=1.7.4" }, { name = "pgvector", specifier = ">=0.4.2" }, @@ -424,6 +429,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/81/d8c22cd7e5e1c6a7d48e41a1d1d46c92f17dae70a54d9814f746e6027dec/bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", size = 152930, upload-time = "2022-10-09T15:36:34.635Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + [[package]] name = "billiard" version = "4.2.4" @@ -641,6 +659,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + [[package]] name = "commitizen" version = "4.16.3" @@ -878,6 +908,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1045,6 +1083,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -1234,6 +1284,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] +[[package]] +name = "magika" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/f3/3d1dcdd7b9c41d589f5cff252d32ed91cdf86ba84391cfc81d9d8773571d/magika-0.6.3.tar.gz", hash = "sha256:7cc52aa7359af861957043e2bf7265ed4741067251c104532765cd668c0c0cb1", size = 3042784, upload-time = "2025-10-30T15:22:34.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/e4/35c323beb3280482c94299d61626116856ac2d4ec16ecef50afc4fdd4291/magika-0.6.3-py3-none-any.whl", hash = "sha256:eda443d08006ee495e02083b32e51b98cb3696ab595a7d13900d8e2ef506ec9d", size = 2969474, upload-time = "2025-10-30T15:22:25.298Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/132b0d7cd51c02c39fd52658a5896276c30c8cc2fd453270b19db8c40f7e/magika-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:86901e64b05dde5faff408c9b8245495b2e1fd4c226e3393d3d2a3fee65c504b", size = 13358841, upload-time = "2025-10-30T15:22:27.413Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/5ed859be502903a68b7b393b17ae0283bf34195cfcca79ce2dc25b9290e7/magika-0.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3d9661eedbdf445ac9567e97e7ceefb93545d77a6a32858139ea966b5806fb64", size = 15367335, upload-time = "2025-10-30T15:22:29.907Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8ee7d644affa3b80efdd623a3d75865c8f058f3950cb87fb0c48e3559bc/magika-0.6.3-py3-none-win_amd64.whl", hash = "sha256:e57f75674447b20cab4db928ae58ab264d7d8582b55183a0b876711c2b2787f3", size = 12692831, upload-time = "2025-10-30T15:22:32.063Z" }, +] + [[package]] name = "mako" version = "1.3.12" @@ -1258,6 +1326,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] +[[package]] +name = "markdownify" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, +] + +[[package]] +name = "markitdown" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "charset-normalizer" }, + { name = "defusedxml" }, + { name = "magika" }, + { name = "markdownify" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/b7/91fe0e2df07107ab701a15c8ad3213135707e4d6206ae9bd8f457a7ad86a/markitdown-0.1.6.tar.gz", hash = "sha256:e5bdbaffd971b29598c7c39ef0e9afce2f08c0751fbfa4e4257678ebaf8cfc7e", size = 50795, upload-time = "2026-05-26T22:43:59.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/30/8031f183ee86ea8ac4e7ea1296bab4cca1bee2fd036a26df69764eb7ca74/markitdown-0.1.6-py3-none-any.whl", hash = "sha256:07b2d5bf87b5c53e13a9f2fdc440df8ccc85e26f40c1e557781727b700049775", size = 70032, upload-time = "2026-05-26T22:44:03.209Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1319,6 +1417,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "msgpack" version = "1.2.0" @@ -1536,6 +1643,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" }, ] +[[package]] +name = "onnxruntime" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/71/c5d980ac4189589267a06f758bd6c5667d07e55656bed6c6c0580733ad07/onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc", size = 31007574, upload-time = "2024-11-21T00:49:23.225Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13bbd9489be2a6944f4a940084bfe388f1100472f38c07080a46fbd4ab96/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be", size = 11951459, upload-time = "2024-11-21T00:49:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/4454ae122874fd52bbb8a961262de81c5f932edeb1b72217f594c700d6ef/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3", size = 13331620, upload-time = "2024-11-21T00:49:28.875Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e0/50db43188ca1c945decaa8fc2a024c33446d31afed40149897d4f9de505f/onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16", size = 11331758, upload-time = "2024-11-21T00:49:31.417Z" }, + { url = "https://files.pythonhosted.org/packages/d8/55/3821c5fd60b52a6c82a00bba18531793c93c4addfe64fbf061e235c5617a/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8", size = 11950342, upload-time = "2024-11-21T00:49:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/14/56/fd990ca222cef4f9f4a9400567b9a15b220dee2eafffb16b2adbc55c8281/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b", size = 13337040, upload-time = "2024-11-21T00:49:37.271Z" }, +] + [[package]] name = "openai" version = "2.43.0" @@ -1781,6 +1909,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + [[package]] name = "py-serializable" version = "2.1.0" @@ -1910,6 +2053,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, +] + [[package]] name = "pytest" version = "9.1.0" @@ -2254,6 +2406,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.51" @@ -2313,6 +2474,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "termcolor" version = "3.3.0" From 69414bbc8a34aad89d7d18287eb3fec32a02fd5a Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Wed, 24 Jun 2026 03:11:27 +0300 Subject: [PATCH 56/81] feat(processing): add unsupported document format exception --- src/ai_notes_api/exceptions/__init__.py | 2 ++ .../exceptions/document_processing.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/ai_notes_api/exceptions/__init__.py b/src/ai_notes_api/exceptions/__init__.py index 28885fa..36f6f71 100644 --- a/src/ai_notes_api/exceptions/__init__.py +++ b/src/ai_notes_api/exceptions/__init__.py @@ -17,6 +17,7 @@ InvalidChunkSizeError, InvalidOverlapError, OverlapGreaterThanOrEqualChunkSizeError, + UnsupportedDocumentFormatError, ) from ai_notes_api.exceptions.generation_job import ( GenerationInProgressError, @@ -53,4 +54,5 @@ "InvalidChunkSizeError", "InvalidOverlapError", "OverlapGreaterThanOrEqualChunkSizeError", + "UnsupportedDocumentFormatError", ] diff --git a/src/ai_notes_api/exceptions/document_processing.py b/src/ai_notes_api/exceptions/document_processing.py index 857af38..b6ffadf 100644 --- a/src/ai_notes_api/exceptions/document_processing.py +++ b/src/ai_notes_api/exceptions/document_processing.py @@ -48,3 +48,19 @@ class OverlapGreaterThanOrEqualChunkSizeError(AppException): def __init__(self) -> None: """Initialize overlap and chunk_size validation exception.""" super().__init__("overlap должен быть меньше chunk_size") + + +class UnsupportedDocumentFormatError(AppException): + """Exception raised when document format is not supported.""" + + status_code: int = 415 + code: str = "UNSUPPORTED_DOCUMENT_FORMAT" + + def __init__(self, content_type: str | None = None) -> None: + """Initialize unsupported document format exception.""" + message = "Unsupported document format" + + if content_type: + message = f"Unsupported document format: {content_type}" + + super().__init__(message) From 79579cc4cfac16c39e82141ae757e251e562f0f7 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Wed, 24 Jun 2026 03:27:26 +0300 Subject: [PATCH 57/81] feat(processing): add document text extractor --- .../services/document_processing.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/ai_notes_api/services/document_processing.py b/src/ai_notes_api/services/document_processing.py index e36d2fa..dabd957 100644 --- a/src/ai_notes_api/services/document_processing.py +++ b/src/ai_notes_api/services/document_processing.py @@ -7,10 +7,12 @@ """ import hashlib +from io import BytesIO from uuid import UUID import tiktoken from loguru import logger +from markitdown import MarkItDown, StreamInfo, UnsupportedFormatException from ai_notes_api.core import settings from ai_notes_api.db.models import Document, DocumentChunk, DocumentStatus @@ -19,6 +21,7 @@ InvalidChunkSizeError, InvalidOverlapError, OverlapGreaterThanOrEqualChunkSizeError, + UnsupportedDocumentFormatError, ) from ai_notes_api.llm import EmbeddingClient from ai_notes_api.repositories import DocumentChunkRepository, DocumentRepository @@ -63,6 +66,7 @@ def __init__( self.chunks = chunk_repository self.storage = storage self.embeddings = embeddings + self.md = MarkItDown() async def process_document(self, document_id: UUID) -> Document: """Process a document into embedded chunks. @@ -132,17 +136,42 @@ async def process_document(self, document_id: UUID) -> Document: return document async def _extract_text(self, data: bytes, content_type: str) -> str: - """Extract plain text from raw document content. + """Extract markdown text from raw document bytes. Args: - data (bytes): Raw document content. - content_type (str): MIME type of the document used to select the - appropriate extraction strategy. + data (bytes): Raw document content to convert. + content_type (str): MIME type of the document. Returns: - str: Extracted plain text. + str: Extracted document text as markdown. + + Raises: + UnsupportedDocumentFormatError: If MarkItDown does not support the + given content type. """ - raise NotImplementedError + logger.debug( + "Extracting text: content_type={}, size={} bytes", + content_type, + len(data), + ) + + try: + result = self.md.convert_stream( + BytesIO(data), + stream_info=StreamInfo(mimetype=content_type), + ) + + except UnsupportedFormatException as exc: + logger.warning("Unsupported document format: content_type={}", content_type) + raise UnsupportedDocumentFormatError(content_type) from exc + + logger.debug( + "Text extraction finished: content_type={}, chars={}", + content_type, + len(result.markdown), + ) + + return result.markdown def _chunk_text( self, @@ -175,6 +204,13 @@ def _chunk_text( if overlap >= chunk_size: raise OverlapGreaterThanOrEqualChunkSizeError() + logger.debug( + "Chunking text: chars={}, chunk_size={}, overlap={}", + len(text), + chunk_size, + overlap, + ) + encoding = tiktoken.get_encoding(settings.tiktoken_encoding_name) tokens = encoding.encode(text) @@ -194,6 +230,12 @@ def _chunk_text( if end >= len(tokens): break + logger.debug( + "Chunking finished: tokens={}, chunks={}", + len(tokens), + len(chunks), + ) + return chunks async def _mark_ready(self, document: Document) -> Document: From f3e2cce7657f4e9ac3a145b74ac150b704e1e97a Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Wed, 24 Jun 2026 03:43:45 +0300 Subject: [PATCH 58/81] feat(proceesing): rewrite sync functions to async --- .../services/document_processing.py | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/src/ai_notes_api/services/document_processing.py b/src/ai_notes_api/services/document_processing.py index dabd957..03f7dc1 100644 --- a/src/ai_notes_api/services/document_processing.py +++ b/src/ai_notes_api/services/document_processing.py @@ -6,6 +6,7 @@ updating the document status accordingly. """ +import asyncio import hashlib from io import BytesIO from uuid import UUID @@ -66,7 +67,6 @@ def __init__( self.chunks = chunk_repository self.storage = storage self.embeddings = embeddings - self.md = MarkItDown() async def process_document(self, document_id: UUID) -> Document: """Process a document into embedded chunks. @@ -94,7 +94,7 @@ async def process_document(self, document_id: UUID) -> Document: data = await self.storage.download_file(document.storage_object_name) text = await self._extract_text(data, document.content_type) - text_chunks = self._chunk_text(text) + text_chunks = await self._chunk_text(text) embeddings = await self.embeddings.create_embedding(text_chunks) @@ -130,10 +130,9 @@ async def process_document(self, document_id: UUID) -> Document: raise - else: - logger.info("Document processing finished: id={}", document_id) + logger.info("Document processing finished: id={}", document_id) - return document + return document async def _extract_text(self, data: bytes, content_type: str) -> str: """Extract markdown text from raw document bytes. @@ -155,25 +154,35 @@ async def _extract_text(self, data: bytes, content_type: str) -> str: len(data), ) - try: - result = self.md.convert_stream( - BytesIO(data), - stream_info=StreamInfo(mimetype=content_type), - ) + def _convert() -> str: + md = MarkItDown() + + try: + result = md.convert_stream( + BytesIO(data), + stream_info=StreamInfo(mimetype=content_type), + ) + + except UnsupportedFormatException as exc: + logger.warning( + "Unsupported document format: content_type={}", content_type + ) + raise UnsupportedDocumentFormatError(content_type) from exc + + return result.markdown - except UnsupportedFormatException as exc: - logger.warning("Unsupported document format: content_type={}", content_type) - raise UnsupportedDocumentFormatError(content_type) from exc + loop = asyncio.get_running_loop() + markdown = await loop.run_in_executor(None, _convert) logger.debug( "Text extraction finished: content_type={}, chars={}", content_type, - len(result.markdown), + len(markdown), ) - return result.markdown + return markdown - def _chunk_text( + async def _chunk_text( self, text: str, chunk_size: int = 1000, @@ -211,30 +220,32 @@ def _chunk_text( overlap, ) - encoding = tiktoken.get_encoding(settings.tiktoken_encoding_name) + def _chunk() -> list[str]: + encoding = tiktoken.get_encoding(settings.tiktoken_encoding_name) - tokens = encoding.encode(text) - chunks = [] + tokens = encoding.encode(text) + chunks = [] - step = chunk_size - overlap + step = chunk_size - overlap - for start in range(0, len(tokens), step): - end = start + chunk_size - chunk_tokens = tokens[start:end] + for start in range(0, len(tokens), step): + end = start + chunk_size + chunk_tokens = tokens[start:end] - chunk = encoding.decode(chunk_tokens).strip() + chunk = encoding.decode(chunk_tokens).strip() - if chunk: - chunks.append(chunk) + if chunk: + chunks.append(chunk) - if end >= len(tokens): - break + if end >= len(tokens): + break - logger.debug( - "Chunking finished: tokens={}, chunks={}", - len(tokens), - len(chunks), - ) + return chunks + + loop = asyncio.get_running_loop() + chunks = await loop.run_in_executor(None, _chunk) + + logger.debug("Chunking finished: chunks={}", len(chunks)) return chunks From ac497f779095151e5fd666da040d9132ee75d20d Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 02:54:52 +0300 Subject: [PATCH 59/81] refactor(processing): decompose processing func --- .../services/document_processing.py | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/ai_notes_api/services/document_processing.py b/src/ai_notes_api/services/document_processing.py index 03f7dc1..4f99d67 100644 --- a/src/ai_notes_api/services/document_processing.py +++ b/src/ai_notes_api/services/document_processing.py @@ -98,28 +98,7 @@ async def process_document(self, document_id: UUID) -> Document: embeddings = await self.embeddings.create_embedding(text_chunks) - chunks = [] - - for chunk_index in range(len(text_chunks)): - text_chunk = text_chunks[chunk_index] - embedding = embeddings[chunk_index] - - chunk_hash = hashlib.sha256(text_chunk.encode()) - - chunks.append( - DocumentChunk( - user_id=document.user_id, - session_id=document.session_id, - document_id=document.id, - chunk_index=chunk_index, - content=text_chunk, - content_hash=(chunk_hash).hexdigest(), - embedding=embedding, - embedding_model=settings.open_ai_embedding_model, - ) - ) - - await self.chunks.create_many(chunks) + await self._persist_chunks(document, text_chunks, embeddings) document = await self._mark_ready(document) @@ -134,6 +113,43 @@ async def process_document(self, document_id: UUID) -> Document: return document + async def _persist_chunks( + self, + document: Document, + text_chunks: list[str], + embeddings: list[list[float]], + ) -> None: + """Build and persist document chunks from text and embeddings. + + Args: + document (Document): Source document the chunks belong to. + text_chunks (list[str]): Ordered text chunks to persist. + embeddings (list[list[float]]): Embedding vectors aligned with + ``text_chunks`` by index. + """ + chunks = [] + + for chunk_index in range(len(text_chunks)): + text_chunk = text_chunks[chunk_index] + embedding = embeddings[chunk_index] + + chunk_hash = hashlib.sha256(text_chunk.encode()) + + chunks.append( + DocumentChunk( + user_id=document.user_id, + session_id=document.session_id, + document_id=document.id, + chunk_index=chunk_index, + content=text_chunk, + content_hash=(chunk_hash).hexdigest(), + embedding=embedding, + embedding_model=settings.open_ai_embedding_model, + ) + ) + + await self.chunks.create_many(chunks) + async def _extract_text(self, data: bytes, content_type: str) -> str: """Extract markdown text from raw document bytes. From 8c8a84a9db7bccdcdd7e6d52d66a1495171b05b4 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 02:55:29 +0300 Subject: [PATCH 60/81] refactor(messages): rename router file --- src/ai_notes_api/api/v1/{message.py => messages.py} | 0 src/ai_notes_api/api/v1/router.py | 4 ++-- tests/api/test_message_route.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/ai_notes_api/api/v1/{message.py => messages.py} (100%) diff --git a/src/ai_notes_api/api/v1/message.py b/src/ai_notes_api/api/v1/messages.py similarity index 100% rename from src/ai_notes_api/api/v1/message.py rename to src/ai_notes_api/api/v1/messages.py diff --git a/src/ai_notes_api/api/v1/router.py b/src/ai_notes_api/api/v1/router.py index b3de9d7..2c54677 100644 --- a/src/ai_notes_api/api/v1/router.py +++ b/src/ai_notes_api/api/v1/router.py @@ -12,7 +12,7 @@ completions, generation_jobs, healthcheck, - message, + messages, notes, ) @@ -24,6 +24,6 @@ router.include_router(auth.router) router.include_router(notes.router) router.include_router(chat_sessions.router) -router.include_router(message.router) +router.include_router(messages.router) router.include_router(completions.router) router.include_router(generation_jobs.router) diff --git a/tests/api/test_message_route.py b/tests/api/test_message_route.py index 09a0fc8..7ecebfc 100644 --- a/tests/api/test_message_route.py +++ b/tests/api/test_message_route.py @@ -9,7 +9,7 @@ from fastapi.testclient import TestClient from ai_notes_api.api.v1.dependencies import get_current_user, get_message_service -from ai_notes_api.api.v1.message import router +from ai_notes_api.api.v1.messages import router from ai_notes_api.db.models import MessageRole, User from ai_notes_api.schemas import MessageResponseSchema From 43e7fcf1ae68e21f95091226c749ec574f3a5ae8 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 02:57:15 +0300 Subject: [PATCH 61/81] feat(documents): add document service dependency --- src/ai_notes_api/api/v1/dependencies.py | 40 ++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/ai_notes_api/api/v1/dependencies.py b/src/ai_notes_api/api/v1/dependencies.py index e07f1b1..d3dc383 100644 --- a/src/ai_notes_api/api/v1/dependencies.py +++ b/src/ai_notes_api/api/v1/dependencies.py @@ -4,7 +4,7 @@ resolving authenticated users, and accessing shared application clients. """ -from typing import Annotated +from typing import Annotated, Any from uuid import UUID from fastapi import Depends @@ -20,6 +20,8 @@ from ai_notes_api.repositories import ( ChatMemoryRepository, ChatSessionRepository, + DocumentProcessingJobRepository, + DocumentRepository, GenerationJobRepository, MessageRepository, NoteRepository, @@ -29,11 +31,13 @@ AuthService, ChatMemoryService, ChatSessionService, + DocumentService, JobService, LLMService, MessageService, NoteService, ) +from ai_notes_api.storage import DocumentStorage, get_s3_client oauth2_scheme = OAuth2PasswordBearer( tokenUrl="/api/v1/auth/login", @@ -241,3 +245,37 @@ def get_memory_service( repository = ChatMemoryRepository(session) return ChatMemoryService(repository) + + +def get_document_service( + db_session: Annotated[AsyncSession, Depends(get_db)], + s3_client: Annotated[Any, Depends(get_s3_client)], +) -> DocumentService: + """Provide a document service instance. + + Args: + db_session (AsyncSession): Asynchronous database session provided by + FastAPI dependency injection. + s3_client (Any): S3 client provided by FastAPI dependency injection. + + Returns: + DocumentService: Configured document service instance. + """ + documents = DocumentRepository(db_session) + processing = DocumentProcessingJobRepository(db_session) + sessions = ChatSessionRepository(db_session) + memories = ChatMemoryRepository(db_session) + + sessions_service = ChatSessionService( + session_repository=sessions, + memory_repository=memories, + ) + + storage = DocumentStorage(s3_client) + + return DocumentService( + document_repository=documents, + processing_repository=processing, + session_service=sessions_service, + storage=storage, + ) From c5704a2bfe67aabad6757c6e716b6227a7e34489 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:02:07 +0300 Subject: [PATCH 62/81] refactor(llm): rename models module to schemas --- src/ai_notes_api/api/v1/completions.py | 2 +- src/ai_notes_api/llm/__init__.py | 7 ++++++- src/ai_notes_api/llm/client.py | 2 +- src/ai_notes_api/llm/{models.py => schemas.py} | 0 src/ai_notes_api/memory/extractor.py | 2 +- src/ai_notes_api/memory/prompt_builder.py | 2 +- src/ai_notes_api/memory/summarizer.py | 2 +- src/ai_notes_api/services/chat_memory.py | 2 +- src/ai_notes_api/services/llm_service.py | 2 +- tests/api/test_completions_route.py | 2 +- tests/memory/test_extractor.py | 2 +- tests/memory/test_prompt_builder.py | 2 +- tests/memory/test_summarizer.py | 2 +- tests/services/test_chat_memory_service.py | 2 +- 14 files changed, 18 insertions(+), 13 deletions(-) rename src/ai_notes_api/llm/{models.py => schemas.py} (100%) diff --git a/src/ai_notes_api/api/v1/completions.py b/src/ai_notes_api/api/v1/completions.py index fd49016..e49eec4 100644 --- a/src/ai_notes_api/api/v1/completions.py +++ b/src/ai_notes_api/api/v1/completions.py @@ -16,7 +16,7 @@ from ai_notes_api.api.v1.dependencies import get_current_user, get_llm_service from ai_notes_api.db.models import User -from ai_notes_api.llm.models import LLMStreamEvent +from ai_notes_api.llm.schemas import LLMStreamEvent from ai_notes_api.schemas import ErrorResponseSchema, UserMessageCreateSchema from ai_notes_api.services import LLMService diff --git a/src/ai_notes_api/llm/__init__.py b/src/ai_notes_api/llm/__init__.py index ca9ba36..b2bc157 100644 --- a/src/ai_notes_api/llm/__init__.py +++ b/src/ai_notes_api/llm/__init__.py @@ -6,7 +6,12 @@ from ai_notes_api.llm.client import LLMClient from ai_notes_api.llm.embeddings import EmbeddingClient -from ai_notes_api.llm.models import LLMMessage, LLMResponse, LLMStreamEvent, LLMToolCall +from ai_notes_api.llm.schemas import ( + LLMMessage, + LLMResponse, + LLMStreamEvent, + LLMToolCall, +) __all__ = [ "LLMClient", diff --git a/src/ai_notes_api/llm/client.py b/src/ai_notes_api/llm/client.py index 9376e68..f846cb9 100644 --- a/src/ai_notes_api/llm/client.py +++ b/src/ai_notes_api/llm/client.py @@ -12,7 +12,7 @@ from openai import AsyncOpenAI from ai_notes_api.core import settings -from ai_notes_api.llm.models import LLMResponse, LLMStreamEvent, LLMToolCall +from ai_notes_api.llm.schemas import LLMResponse, LLMStreamEvent, LLMToolCall class LLMClient: diff --git a/src/ai_notes_api/llm/models.py b/src/ai_notes_api/llm/schemas.py similarity index 100% rename from src/ai_notes_api/llm/models.py rename to src/ai_notes_api/llm/schemas.py diff --git a/src/ai_notes_api/memory/extractor.py b/src/ai_notes_api/memory/extractor.py index daebcfe..47e4cf1 100644 --- a/src/ai_notes_api/memory/extractor.py +++ b/src/ai_notes_api/memory/extractor.py @@ -15,7 +15,7 @@ ) from ai_notes_api.core import settings -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage from ai_notes_api.memory.prompts import FACT_EXTRACTION_PROMPT diff --git a/src/ai_notes_api/memory/prompt_builder.py b/src/ai_notes_api/memory/prompt_builder.py index 9292c3b..7554368 100644 --- a/src/ai_notes_api/memory/prompt_builder.py +++ b/src/ai_notes_api/memory/prompt_builder.py @@ -8,7 +8,7 @@ from dataclasses import asdict from typing import Any -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage class PromptBuilder: diff --git a/src/ai_notes_api/memory/summarizer.py b/src/ai_notes_api/memory/summarizer.py index eef5265..491d3d4 100644 --- a/src/ai_notes_api/memory/summarizer.py +++ b/src/ai_notes_api/memory/summarizer.py @@ -8,7 +8,7 @@ from openai.types.responses import ResponseInputParam from ai_notes_api.core import settings -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage from ai_notes_api.memory.prompts import SUMMARY_PROMPT diff --git a/src/ai_notes_api/services/chat_memory.py b/src/ai_notes_api/services/chat_memory.py index 0767883..77c7d3c 100644 --- a/src/ai_notes_api/services/chat_memory.py +++ b/src/ai_notes_api/services/chat_memory.py @@ -12,7 +12,7 @@ ChatMemoryDependenciesNotConfiguredError, ChatMemoryNotFoundError, ) -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage from ai_notes_api.memory import MemoryExtractor, MemorySummarizer from ai_notes_api.repositories import ChatMemoryRepository, MessageRepository diff --git a/src/ai_notes_api/services/llm_service.py b/src/ai_notes_api/services/llm_service.py index a043c78..30018d2 100644 --- a/src/ai_notes_api/services/llm_service.py +++ b/src/ai_notes_api/services/llm_service.py @@ -10,7 +10,7 @@ from ai_notes_api.core import settings from ai_notes_api.db.models import Message from ai_notes_api.llm import LLMClient -from ai_notes_api.llm.models import LLMMessage, LLMResponse, LLMStreamEvent +from ai_notes_api.llm.schemas import LLMMessage, LLMResponse, LLMStreamEvent from ai_notes_api.memory import PromptBuilder from ai_notes_api.schemas import ( AssistantMessageCreateSchema, diff --git a/tests/api/test_completions_route.py b/tests/api/test_completions_route.py index 0e6c418..ae77bed 100644 --- a/tests/api/test_completions_route.py +++ b/tests/api/test_completions_route.py @@ -12,7 +12,7 @@ from ai_notes_api.api.v1.completions import llm_event_to_sse, router from ai_notes_api.api.v1.dependencies import get_current_user, get_llm_service from ai_notes_api.db.models import User -from ai_notes_api.llm.models import LLMResponse, LLMStreamEvent +from ai_notes_api.llm.schemas import LLMResponse, LLMStreamEvent TEST_USER_ID = UUID("11111111-1111-1111-1111-111111111111") TEST_SESSION_ID = UUID("22222222-2222-2222-2222-222222222222") diff --git a/tests/memory/test_extractor.py b/tests/memory/test_extractor.py index fd588f7..bcc65bb 100644 --- a/tests/memory/test_extractor.py +++ b/tests/memory/test_extractor.py @@ -9,7 +9,7 @@ from ai_notes_api.core import settings from ai_notes_api.db.models import MessageRole -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage from ai_notes_api.memory.extractor import MemoryExtractor from ai_notes_api.memory.prompts import FACT_EXTRACTION_PROMPT diff --git a/tests/memory/test_prompt_builder.py b/tests/memory/test_prompt_builder.py index 89c874a..992feef 100644 --- a/tests/memory/test_prompt_builder.py +++ b/tests/memory/test_prompt_builder.py @@ -3,7 +3,7 @@ import json from ai_notes_api.db.models import MessageRole -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage from ai_notes_api.memory.prompt_builder import PromptBuilder diff --git a/tests/memory/test_summarizer.py b/tests/memory/test_summarizer.py index ce0ae0e..7aaa12c 100644 --- a/tests/memory/test_summarizer.py +++ b/tests/memory/test_summarizer.py @@ -7,7 +7,7 @@ from ai_notes_api.core import settings from ai_notes_api.db.models import MessageRole -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage from ai_notes_api.memory.prompts import SUMMARY_PROMPT from ai_notes_api.memory.summarizer import MemorySummarizer diff --git a/tests/services/test_chat_memory_service.py b/tests/services/test_chat_memory_service.py index d5494ca..a9e0d28 100644 --- a/tests/services/test_chat_memory_service.py +++ b/tests/services/test_chat_memory_service.py @@ -12,7 +12,7 @@ ChatMemoryDependenciesNotConfiguredError, ChatMemoryNotFoundError, ) -from ai_notes_api.llm.models import LLMMessage +from ai_notes_api.llm.schemas import LLMMessage from ai_notes_api.memory import MemoryExtractor, MemorySummarizer from ai_notes_api.repositories import ChatMemoryRepository, MessageRepository from ai_notes_api.services import ChatMemoryService From 753af48cca44497eeeb2cc56afbb5011da3fcf97 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:02:25 +0300 Subject: [PATCH 63/81] feat(ingestion): add document text extractor and chunker --- src/ai_notes_api/ingestion/__init__.py | 12 +++ src/ai_notes_api/ingestion/chunking.py | 107 +++++++++++++++++++ src/ai_notes_api/ingestion/schemas.py | 24 +++++ src/ai_notes_api/ingestion/text_extractor.py | 69 ++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 src/ai_notes_api/ingestion/__init__.py create mode 100644 src/ai_notes_api/ingestion/chunking.py create mode 100644 src/ai_notes_api/ingestion/schemas.py create mode 100644 src/ai_notes_api/ingestion/text_extractor.py diff --git a/src/ai_notes_api/ingestion/__init__.py b/src/ai_notes_api/ingestion/__init__.py new file mode 100644 index 0000000..b4c10dd --- /dev/null +++ b/src/ai_notes_api/ingestion/__init__.py @@ -0,0 +1,12 @@ +"""Document ingestion package. + +This package provides the building blocks that turn a raw uploaded document into +embeddable text: extracting text from binary document formats and splitting that +text into token-based overlapping chunks. +""" + +from ai_notes_api.ingestion.chunking import TokenTextChunker +from ai_notes_api.ingestion.schemas import TextChunk +from ai_notes_api.ingestion.text_extractor import TextExtractor + +__all__ = ["TextExtractor", "TokenTextChunker", "TextChunk"] diff --git a/src/ai_notes_api/ingestion/chunking.py b/src/ai_notes_api/ingestion/chunking.py new file mode 100644 index 0000000..6a932e3 --- /dev/null +++ b/src/ai_notes_api/ingestion/chunking.py @@ -0,0 +1,107 @@ +"""Text chunking module. + +This module provides :class:`TokenTextChunker`, which splits extracted text into +token-based overlapping chunks suitable for embedding. +""" + +import asyncio +import hashlib + +import tiktoken +from loguru import logger + +from ai_notes_api.core import settings +from ai_notes_api.exceptions import ( + InvalidChunkSizeError, + InvalidOverlapError, + OverlapGreaterThanOrEqualChunkSizeError, +) +from ai_notes_api.ingestion.schemas import TextChunk + + +class TokenTextChunker: + """Splits text into token-based overlapping chunks. + + Text is tokenized with the configured tiktoken encoding and split into + fixed-size windows that overlap by a configurable number of tokens. + """ + + def __init__(self, chunk_size: int = 1000, overlap: int = 200) -> None: + """Initialize the chunker and validate its configuration. + + Args: + chunk_size (int): Maximum number of tokens in each chunk. + overlap (int): Number of tokens repeated between adjacent chunks. + + Raises: + InvalidChunkSizeError: If `chunk_size` is less than or equal to zero. + InvalidOverlapError: If `overlap` is negative. + OverlapGreaterThanOrEqualChunkSizeError: If `overlap` is greater than + or equal to `chunk_size`. + """ + self.chunk_size = chunk_size + self.overlap = overlap + self.encoding_name = settings.tiktoken_encoding_name + + if chunk_size <= 0: + raise InvalidChunkSizeError() + + if overlap < 0: + raise InvalidOverlapError() + + if overlap >= chunk_size: + raise OverlapGreaterThanOrEqualChunkSizeError() + + async def chunk(self, text: str) -> list[TextChunk]: + """Split extracted text into token-based overlapping chunks. + + Args: + text (str): Plain text to split. + + Returns: + list[TextChunk]: Ordered non-empty text chunks ready for embedding. + """ + logger.debug( + "Chunking text: chars={}, chunk_size={}, overlap={}", + len(text), + self.chunk_size, + self.overlap, + ) + + def _chunk() -> list[TextChunk]: + encoding = tiktoken.get_encoding(self.encoding_name) + + tokens = encoding.encode(text) + chunks = [] + + step = self.chunk_size - self.overlap + + for start in range(0, len(tokens), step): + end = start + self.chunk_size + chunk_tokens = tokens[start:end] + + content = encoding.decode(chunk_tokens).strip() + + if content: + chunks.append( + TextChunk( + index=len(chunks), + content=content, + content_hash=hashlib.sha256( + content.encode("utf-8") + ).hexdigest(), + token_count=len(chunk_tokens), + ) + ) + + if end >= len(tokens): + break + + return chunks + + loop = asyncio.get_running_loop() + chunks = await loop.run_in_executor(None, _chunk) + + logger.debug("Chunking finished: chunks={}", len(chunks)) + + return chunks diff --git a/src/ai_notes_api/ingestion/schemas.py b/src/ai_notes_api/ingestion/schemas.py new file mode 100644 index 0000000..9f361e2 --- /dev/null +++ b/src/ai_notes_api/ingestion/schemas.py @@ -0,0 +1,24 @@ +"""Ingestion data models module. + +This module defines dataclasses representing artifacts produced while ingesting +documents, such as text chunks. +""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class TextChunk: + """Chunk of text extracted from a document during ingestion. + + Attributes: + index (int): Zero-based position of the chunk within the document. + content (str): Text content of the chunk. + content_hash (str): Hash of the content used for deduplication. + token_count (int | None): Number of tokens in the chunk, if known. + """ + + index: int + content: str + content_hash: str + token_count: int | None = None diff --git a/src/ai_notes_api/ingestion/text_extractor.py b/src/ai_notes_api/ingestion/text_extractor.py new file mode 100644 index 0000000..1ac39e2 --- /dev/null +++ b/src/ai_notes_api/ingestion/text_extractor.py @@ -0,0 +1,69 @@ +"""Text extraction module. + +This module provides :class:`TextExtractor`, which converts raw document bytes +of various formats into markdown text using MarkItDown. +""" + +import asyncio +from io import BytesIO + +from loguru import logger +from markitdown import MarkItDown, StreamInfo, UnsupportedFormatException + +from ai_notes_api.exceptions import UnsupportedDocumentFormatError + + +class TextExtractor: + """Extracts markdown text from raw document bytes. + + Conversion is delegated to MarkItDown and runs in a thread pool executor so + that the synchronous extraction does not block the event loop. + """ + + async def extract(self, data: bytes, content_type: str) -> str: + """Extract markdown text from raw document bytes. + + Args: + data (bytes): Raw document content to convert. + content_type (str): MIME type of the document. + + Returns: + str: Extracted document text as markdown. + + Raises: + UnsupportedDocumentFormatError: If MarkItDown does not support the + given content type. + """ + logger.debug( + "Extracting text: content_type={}, size={} bytes", + content_type, + len(data), + ) + + def _convert() -> str: + md = MarkItDown() + + try: + result = md.convert_stream( + BytesIO(data), + stream_info=StreamInfo(mimetype=content_type), + ) + + except UnsupportedFormatException as exc: + logger.warning( + "Unsupported document format: content_type={}", content_type + ) + raise UnsupportedDocumentFormatError(content_type) from exc + + return result.markdown + + loop = asyncio.get_running_loop() + markdown = await loop.run_in_executor(None, _convert) + + logger.debug( + "Text extraction finished: content_type={}, chars={}", + content_type, + len(markdown), + ) + + return markdown From 095978164cf04a6f94351a90d0ab6d916104c9b7 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:02:47 +0300 Subject: [PATCH 64/81] refactor(processing): use ingestion module in document processing service --- src/ai_notes_api/exceptions/__init__.py | 2 + .../exceptions/document_processing.py | 11 ++ .../services/document_processing.py | 182 ++++-------------- 3 files changed, 50 insertions(+), 145 deletions(-) diff --git a/src/ai_notes_api/exceptions/__init__.py b/src/ai_notes_api/exceptions/__init__.py index 36f6f71..ceca6f6 100644 --- a/src/ai_notes_api/exceptions/__init__.py +++ b/src/ai_notes_api/exceptions/__init__.py @@ -13,6 +13,7 @@ from ai_notes_api.exceptions.chat_session import ChatSessionNotFoundError from ai_notes_api.exceptions.document import DocumentNotFoundError from ai_notes_api.exceptions.document_processing import ( + ChunkEmbeddingCountMismatchError, DocumentProcessingJobNotFoundError, InvalidChunkSizeError, InvalidOverlapError, @@ -50,6 +51,7 @@ "MemoryInProgressError", "ChatMemoryDependenciesNotConfiguredError", "DocumentNotFoundError", + "ChunkEmbeddingCountMismatchError", "DocumentProcessingJobNotFoundError", "InvalidChunkSizeError", "InvalidOverlapError", diff --git a/src/ai_notes_api/exceptions/document_processing.py b/src/ai_notes_api/exceptions/document_processing.py index b6ffadf..61a55d1 100644 --- a/src/ai_notes_api/exceptions/document_processing.py +++ b/src/ai_notes_api/exceptions/document_processing.py @@ -50,6 +50,17 @@ def __init__(self) -> None: super().__init__("overlap должен быть меньше chunk_size") +class ChunkEmbeddingCountMismatchError(AppException): + """Exception raised when chunk and embedding counts do not match.""" + + status_code: int = 500 + code: str = "CHUNK_EMBEDDING_COUNT_MISMATCH" + + def __init__(self) -> None: + """Initialize the chunk and embedding count mismatch exception.""" + super().__init__("Chunks and embeddings count mismatch") + + class UnsupportedDocumentFormatError(AppException): """Exception raised when document format is not supported.""" diff --git a/src/ai_notes_api/services/document_processing.py b/src/ai_notes_api/services/document_processing.py index 4f99d67..afe748f 100644 --- a/src/ai_notes_api/services/document_processing.py +++ b/src/ai_notes_api/services/document_processing.py @@ -6,24 +6,18 @@ updating the document status accordingly. """ -import asyncio -import hashlib -from io import BytesIO from uuid import UUID -import tiktoken from loguru import logger -from markitdown import MarkItDown, StreamInfo, UnsupportedFormatException from ai_notes_api.core import settings from ai_notes_api.db.models import Document, DocumentChunk, DocumentStatus from ai_notes_api.exceptions import ( + ChunkEmbeddingCountMismatchError, DocumentNotFoundError, - InvalidChunkSizeError, - InvalidOverlapError, - OverlapGreaterThanOrEqualChunkSizeError, - UnsupportedDocumentFormatError, ) +from ai_notes_api.ingestion import TextExtractor, TokenTextChunker +from ai_notes_api.ingestion.schemas import TextChunk from ai_notes_api.llm import EmbeddingClient from ai_notes_api.repositories import DocumentChunkRepository, DocumentRepository from ai_notes_api.storage import DocumentStorage @@ -40,18 +34,22 @@ class DocumentProcessingService: storage (DocumentStorage): Object storage helper used to download the source file from S3. embeddings (EmbeddingClient): Client used to generate chunk embeddings. + text_extractor (TextExtractor): Extractor used to convert raw document + bytes into text. + chunker (TokenTextChunker): Chunker used to split extracted text into + embeddable chunks. """ - CHUNK_SIZE = 1_000 - CHUNK_OVERLAP = 200 ERROR_MAX_LENGTH = 10_000 - def __init__( + def __init__( # noqa: PLR0913 self, document_repository: DocumentRepository, chunk_repository: DocumentChunkRepository, storage: DocumentStorage, embeddings: EmbeddingClient, + text_extractor: TextExtractor, + chunker: TokenTextChunker, ) -> None: """Initialize the document processing service. @@ -62,11 +60,15 @@ def __init__( used by the service. storage (DocumentStorage): Object storage helper used by the service. embeddings (EmbeddingClient): Embedding client used by the service. + text_extractor (TextExtractor): Text extractor used by the service. + chunker (TokenTextChunker): Text chunker used by the service. """ self.documents = document_repository self.chunks = chunk_repository self.storage = storage self.embeddings = embeddings + self.text_extractor = text_extractor + self.chunker = chunker async def process_document(self, document_id: UUID) -> Document: """Process a document into embedded chunks. @@ -93,19 +95,21 @@ async def process_document(self, document_id: UUID) -> Document: data = await self.storage.download_file(document.storage_object_name) - text = await self._extract_text(data, document.content_type) - text_chunks = await self._chunk_text(text) + text = await self.text_extractor.extract(data, document.content_type) + text_chunks = await self.chunker.chunk(text) - embeddings = await self.embeddings.create_embedding(text_chunks) + embeddings = await self.embeddings.create_embedding( + [chunk.content for chunk in text_chunks] + ) await self._persist_chunks(document, text_chunks, embeddings) - document = await self._mark_ready(document) + document = await self._set_document_ready(document) except Exception as exc: logger.exception("Document processing failed: id={}", document_id) - await self._mark_failed(document, str(exc)) + await self._set_document_failed(document, str(exc)) raise @@ -116,33 +120,36 @@ async def process_document(self, document_id: UUID) -> Document: async def _persist_chunks( self, document: Document, - text_chunks: list[str], + text_chunks: list[TextChunk], embeddings: list[list[float]], ) -> None: """Build and persist document chunks from text and embeddings. Args: document (Document): Source document the chunks belong to. - text_chunks (list[str]): Ordered text chunks to persist. + text_chunks (list[TextChunk]): Ordered text chunks to persist. embeddings (list[list[float]]): Embedding vectors aligned with ``text_chunks`` by index. - """ - chunks = [] - for chunk_index in range(len(text_chunks)): - text_chunk = text_chunks[chunk_index] - embedding = embeddings[chunk_index] + Raises: + ChunkEmbeddingCountMismatchError: If the number of text chunks and + embeddings differ. + """ + if len(text_chunks) != len(embeddings): + raise ChunkEmbeddingCountMismatchError() - chunk_hash = hashlib.sha256(text_chunk.encode()) + chunks = [] + for text_chunk, embedding in zip(text_chunks, embeddings, strict=True): chunks.append( DocumentChunk( user_id=document.user_id, session_id=document.session_id, document_id=document.id, - chunk_index=chunk_index, - content=text_chunk, - content_hash=(chunk_hash).hexdigest(), + chunk_index=text_chunk.index, + content=text_chunk.content, + content_hash=text_chunk.content_hash, + token_count=text_chunk.token_count, embedding=embedding, embedding_model=settings.open_ai_embedding_model, ) @@ -150,122 +157,7 @@ async def _persist_chunks( await self.chunks.create_many(chunks) - async def _extract_text(self, data: bytes, content_type: str) -> str: - """Extract markdown text from raw document bytes. - - Args: - data (bytes): Raw document content to convert. - content_type (str): MIME type of the document. - - Returns: - str: Extracted document text as markdown. - - Raises: - UnsupportedDocumentFormatError: If MarkItDown does not support the - given content type. - """ - logger.debug( - "Extracting text: content_type={}, size={} bytes", - content_type, - len(data), - ) - - def _convert() -> str: - md = MarkItDown() - - try: - result = md.convert_stream( - BytesIO(data), - stream_info=StreamInfo(mimetype=content_type), - ) - - except UnsupportedFormatException as exc: - logger.warning( - "Unsupported document format: content_type={}", content_type - ) - raise UnsupportedDocumentFormatError(content_type) from exc - - return result.markdown - - loop = asyncio.get_running_loop() - markdown = await loop.run_in_executor(None, _convert) - - logger.debug( - "Text extraction finished: content_type={}, chars={}", - content_type, - len(markdown), - ) - - return markdown - - async def _chunk_text( - self, - text: str, - chunk_size: int = 1000, - overlap: int = 200, - ) -> list[str]: - """Split extracted text into token-based overlapping chunks. - - Args: - text (str): Plain text to split. - chunk_size (int): Maximum number of tokens in each chunk. - overlap (int): Number of tokens repeated between adjacent chunks. - - Returns: - list[str]: Ordered non-empty text chunks ready for embedding. - - Raises: - InvalidChunkSizeError: If `chunk_size` is less than or equal to zero. - InvalidOverlapError: If `overlap` is negative. - OverlapGreaterThanOrEqualChunkSizeError: If `overlap` is greater than - or equal to `chunk_size`. - """ - if chunk_size <= 0: - raise InvalidChunkSizeError() - - if overlap < 0: - raise InvalidOverlapError() - - if overlap >= chunk_size: - raise OverlapGreaterThanOrEqualChunkSizeError() - - logger.debug( - "Chunking text: chars={}, chunk_size={}, overlap={}", - len(text), - chunk_size, - overlap, - ) - - def _chunk() -> list[str]: - encoding = tiktoken.get_encoding(settings.tiktoken_encoding_name) - - tokens = encoding.encode(text) - chunks = [] - - step = chunk_size - overlap - - for start in range(0, len(tokens), step): - end = start + chunk_size - chunk_tokens = tokens[start:end] - - chunk = encoding.decode(chunk_tokens).strip() - - if chunk: - chunks.append(chunk) - - if end >= len(tokens): - break - - return chunks - - loop = asyncio.get_running_loop() - chunks = await loop.run_in_executor(None, _chunk) - - logger.debug("Chunking finished: chunks={}", len(chunks)) - - return chunks - - async def _mark_ready(self, document: Document) -> Document: + async def _set_document_ready(self, document: Document) -> Document: """Mark a document as successfully processed. Args: @@ -279,7 +171,7 @@ async def _mark_ready(self, document: Document) -> Document: return await self.documents.update(document) - async def _mark_failed(self, document: Document, error: str) -> Document: + async def _set_document_failed(self, document: Document, error: str) -> Document: """Mark a document as failed. Args: From 2a617e784f9bd888a9324353f9d3123e3db3e0f3 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:02:55 +0300 Subject: [PATCH 65/81] refactor(generation): rename job service and add status helpers --- src/ai_notes_api/services/generation_job.py | 136 +++++++- src/ai_notes_api/workers/tasks/generation.py | 97 ++--- tests/services/test_generation_job_service.py | 330 +++++++++++++----- 3 files changed, 401 insertions(+), 162 deletions(-) diff --git a/src/ai_notes_api/services/generation_job.py b/src/ai_notes_api/services/generation_job.py index 93b7ea6..360b04b 100644 --- a/src/ai_notes_api/services/generation_job.py +++ b/src/ai_notes_api/services/generation_job.py @@ -5,6 +5,7 @@ session invariant shared by asynchronous and streaming generation paths. """ +from datetime import UTC, datetime from uuid import UUID from ai_notes_api.db.models import GenerationJob, GenerationJobStatus @@ -14,7 +15,7 @@ from ai_notes_api.services.chat_session import ChatSessionService -class JobService: +class GenerationJobService: """Service for generation-job-related business operations. Args: @@ -24,20 +25,22 @@ class JobService: access and manage generation locks. """ + ERROR_MAX_LENGTH = 10_000 + def __init__( self, - job_repository: GenerationJobRepository, + generation_repository: GenerationJobRepository, session_service: ChatSessionService, ) -> None: """Initialize the generation job service. Args: - job_repository (GenerationJobRepository): Generation job repository + generation_repository (GenerationJobRepository): Generation job repository used by the service. session_service (ChatSessionService): Chat session service used by the service. """ - self.jobs = job_repository + self.generations = generation_repository self.sessions = session_service async def create_job( @@ -61,14 +64,14 @@ async def create_job( """ await self.sessions.ensure_session_owner(user_id, data.session_id) - generation_job_data = GenerationJob( + generation_data = GenerationJob( user_id=user_id, session_id=data.session_id, input_message=data.message, status=GenerationJobStatus.QUEUED, ) - generation_job = await self.jobs.create(generation_job_data) + generation_job = await self.generations.create(generation_data) await self.sessions.acquire_generation_lock( user_id=user_id, @@ -78,7 +81,27 @@ async def create_job( return generation_job - async def get_by_id(self, user_id: UUID, job_id: UUID) -> GenerationJob: + async def get_by_id(self, job_id: UUID) -> GenerationJob: + """Return generation job by its identifier. + + Args: + user_id (UUID): Unique identifier of the user who owns the job. + job_id (UUID): Unique generation job identifier. + + Returns: + GenerationJob: Matching generation job. + + Raises: + GenerationNotFoundError: If no accessible generation job exists. + """ + generation_job = await self.generations.get_by_id(job_id) + + if generation_job is None: + raise GenerationNotFoundError() + + return generation_job + + async def get_by_id_for_user(self, user_id: UUID, job_id: UUID) -> GenerationJob: """Return a user's generation job by its identifier. Args: @@ -91,7 +114,10 @@ async def get_by_id(self, user_id: UUID, job_id: UUID) -> GenerationJob: Raises: GenerationNotFoundError: If no accessible generation job exists. """ - generation_job = await self.jobs.get_by_id_for_user(user_id, job_id) + generation_job = await self.generations.get_by_id_for_user( + user_id=user_id, + job_id=job_id, + ) if generation_job is None: raise GenerationNotFoundError() @@ -119,7 +145,7 @@ async def get_list( """ await self.sessions.ensure_session_owner(user_id, session_id) - return await self.jobs.get_list(user_id, session_id, filters) + return await self.generations.get_list(user_id, session_id, filters) async def update_job( self, @@ -141,12 +167,98 @@ async def update_job( Raises: GenerationNotFoundError: If no accessible generation job exists. """ - job = await self.get_by_id(user_id, job_id) + generation_job = await self.get_by_id_for_user(user_id, job_id) update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): if value is not None: - setattr(job, field, value) + setattr(generation_job, field, value) + + return await self.generations.update(generation_job) + + async def set_job_running(self, generation_id: UUID) -> GenerationJob: + """Mark a generation job as running and record its start time. + + Args: + generation_id (UUID): Unique generation job identifier. + + Returns: + GenerationJob: Updated generation job. + + Raises: + GenerationNotFoundError: If no generation job exists. + """ + generation_job = await self.get_by_id(generation_id) + + generation_job.status = GenerationJobStatus.RUNNING + generation_job.started_at = datetime.now(UTC) + + return await self.generations.update(generation_job) + + async def set_job_failed( + self, generation_id: UUID, error_message: str | None = None + ) -> GenerationJob: + """Mark a generation job as failed and record the error and finish time. + + The error message is truncated to ``ERROR_MAX_LENGTH`` characters. + + Args: + generation_id (UUID): Unique generation job identifier. + error_message (str | None): Error message describing the failure. + + Returns: + GenerationJob: Updated generation job. + + Raises: + GenerationNotFoundError: If no generation job exists. + """ + generation_job = await self.get_by_id(generation_id) + + generation_job.status = GenerationJobStatus.FAILED + generation_job.error = error_message[: self.ERROR_MAX_LENGTH] + generation_job.finished_at = datetime.now(UTC) + + return await self.generations.update(generation_job) + + async def set_job_completed( + self, generation_id: UUID, message_id: UUID + ) -> GenerationJob: + """Mark a generation job as completed and link its output message. + + Args: + generation_id (UUID): Unique generation job identifier. + message_id (UUID): Unique identifier of the generated output message. + + Returns: + GenerationJob: Updated generation job. + + Raises: + GenerationNotFoundError: If no generation job exists. + """ + generation_job = await self.get_by_id(generation_id) + + generation_job.status = GenerationJobStatus.COMPLETED + generation_job.output_message_id = message_id + generation_job.finished_at = datetime.now(UTC) + + return await self.generations.update(generation_job) + + async def set_job_cancelled(self, generation_id: UUID) -> GenerationJob: + """Mark a generation job as cancelled and record its finish time. + + Args: + generation_id (UUID): Unique generation job identifier. + + Returns: + GenerationJob: Updated generation job. + + Raises: + GenerationNotFoundError: If no generation job exists. + """ + generation_job = await self.get_by_id(generation_id) + + generation_job.status = GenerationJobStatus.CANCELLED + generation_job.finished_at = datetime.now(UTC) - return await self.jobs.update(job) + return await self.generations.update(generation_job) diff --git a/src/ai_notes_api/workers/tasks/generation.py b/src/ai_notes_api/workers/tasks/generation.py index 41f7ca0..0277a84 100644 --- a/src/ai_notes_api/workers/tasks/generation.py +++ b/src/ai_notes_api/workers/tasks/generation.py @@ -4,15 +4,11 @@ """ import asyncio -from datetime import UTC, datetime from uuid import UUID from loguru import logger -from sqlalchemy.ext.asyncio import AsyncSession -from ai_notes_api.db.models import GenerationJobStatus from ai_notes_api.db.session import worker_session -from ai_notes_api.exceptions.generation_job import GenerationNotFoundError from ai_notes_api.integrations import openai_client from ai_notes_api.llm import LLMClient from ai_notes_api.repositories import ( @@ -25,14 +21,13 @@ from ai_notes_api.schemas.message import UserMessageCreateSchema from ai_notes_api.services import ( ChatSessionService, + GenerationJobService, LLMService, MessageService, NoteService, ) from ai_notes_api.workers.celery_app import celery_app -ERROR_MAX_LENGTH = 10_000 - @celery_app.task(name="generation.run") def run_generation_job(job_id: str) -> None: @@ -60,7 +55,7 @@ async def _run_generation_job(job_id: UUID) -> None: messages_repository = MessageRepository(session) sessions_repository = ChatSessionRepository(session) memories_repository = ChatMemoryRepository(session) - generation_job_repository = GenerationJobRepository(session) + generation_repository = GenerationJobRepository(session) notes_service = NoteService(notes_repository) messages_service = MessageService( @@ -71,15 +66,15 @@ async def _run_generation_job(job_id: UUID) -> None: session_repository=sessions_repository, memory_repository=memories_repository, ) + generation_service = GenerationJobService( + generation_repository=generation_repository + ) - generation_job = await generation_job_repository.get_by_id(job_id) - - if generation_job is None: - raise GenerationNotFoundError() + generation = await generation_service.get_by_id(job_id) message = UserMessageCreateSchema( - session_id=generation_job.session_id, - content=generation_job.input_message, + session_id=generation.session_id, + content=generation.input_message, ) service = LLMService( @@ -90,22 +85,20 @@ async def _run_generation_job(job_id: UUID) -> None: ) try: - logger.info("Generation job started: id={}", job_id) + logger.info("Generation job started: id={}", generation.id) - generation_job.started_at = datetime.now(UTC) - generation_job = await generation_job_repository.update(generation_job) + await generation_service.set_job_running(generation.id) completion = await service.generate_job_response( - user_id=generation_job.user_id, - generation_id=generation_job.id, + user_id=generation.user_id, + generation_id=generation.id, message=message, ) - generation_job.output_message_id = completion.message_id - generation_job.status = GenerationJobStatus.COMPLETED - generation_job.finished_at = datetime.now(UTC) - - await generation_job_repository.update(generation_job) + await generation_service.set_job_completed( + generation_id=generation.id, + message_id=completion.message_id, + ) await session.commit() @@ -116,55 +109,17 @@ async def _run_generation_job(job_id: UUID) -> None: logger.exception("Generation job failed: id={}", job_id) - await _mark_job_failed( - session=session, - job_repository=generation_job_repository, - sessions_service=sessions_service, - job_id=job_id, - error=str(exc), + await generation_service.set_job_failed( + generation_id=generation.id, + error_message=str(exc), ) - raise - - -async def _mark_job_failed( - session: AsyncSession, - job_repository: GenerationJobRepository, - sessions_service: ChatSessionService, - job_id: UUID, - error: str, -) -> None: - """Mark a generation job as failed and release its session lock. - - This is invoked after the job transaction has been rolled back, so it runs - in a fresh transaction to persist the failure state and release the chat - session generation lock that the rolled-back transaction would otherwise - leave held. - - Args: - session (AsyncSession): Database session used to commit the failure state. - job_repository (GenerationJobRepository): Repository used to update the - generation job. - sessions_service (ChatSessionService): Chat session service used to - release the generation lock. - job_id (UUID): Unique generation job identifier. - error (str): Error message describing the failure. - """ - generation_job = await job_repository.get_by_id(job_id) - - if generation_job is None: - return - - generation_job.status = GenerationJobStatus.FAILED - generation_job.error = error[:ERROR_MAX_LENGTH] - generation_job.finished_at = datetime.now(UTC) - - await job_repository.update(generation_job) + await sessions_service.release_generation_lock( + user_id=generation.user_id, + session_id=generation.session_id, + generation_id=generation.id, + ) - await sessions_service.release_generation_lock( - user_id=generation_job.user_id, - session_id=generation_job.session_id, - generation_id=generation_job.id, - ) + await session.commit() - await session.commit() + raise diff --git a/tests/services/test_generation_job_service.py b/tests/services/test_generation_job_service.py index ee1dfbb..bc11320 100644 --- a/tests/services/test_generation_job_service.py +++ b/tests/services/test_generation_job_service.py @@ -19,7 +19,7 @@ GenerationJobUpdateSchema, ) from ai_notes_api.services import ChatSessionService -from ai_notes_api.services.generation_job import JobService +from ai_notes_api.services.generation_job import GenerationJobService TEST_USER_ID = UUID("11111111-1111-1111-1111-111111111111") TEST_USER_ID_2 = UUID("44444444-4444-4444-4444-444444444444") @@ -29,7 +29,7 @@ class FakeChatSessionService: - """Fake chat session service used for testing job service behavior.""" + """Fake chat session service used for testing generation service behavior.""" def __init__(self) -> None: """Initialize fake chat session service.""" @@ -63,18 +63,22 @@ class FakeGenerationJobRepository: def __init__(self) -> None: """Initialize fake repository.""" - self.jobs: dict[UUID, GenerationJob] = {} - self.created_job: GenerationJob | None = None - self.updated_job: GenerationJob | None = None + self.generations: dict[UUID, GenerationJob] = {} + self.created_generation: GenerationJob | None = None + self.updated_generation: GenerationJob | None = None - async def create(self, generation_job: GenerationJob) -> GenerationJob: + async def create(self, generation: GenerationJob) -> GenerationJob: """Create generation job.""" - generation_job.id = TEST_JOB_ID + generation.id = TEST_JOB_ID - self.created_job = generation_job - self.jobs[generation_job.id] = generation_job + self.created_generation = generation + self.generations[generation.id] = generation - return generation_job + return generation + + async def get_by_id(self, job_id: UUID) -> GenerationJob | None: + """Return generation job by its identifier.""" + return self.generations.get(job_id) async def get_by_id_for_user( self, @@ -82,10 +86,10 @@ async def get_by_id_for_user( job_id: UUID, ) -> GenerationJob | None: """Return generation job scoped to the owning user.""" - generation_job = self.jobs.get(job_id) + generation = self.generations.get(job_id) - if generation_job is not None and generation_job.user_id == user_id: - return generation_job + if generation is not None and generation.user_id == user_id: + return generation return None @@ -96,43 +100,51 @@ async def get_list( filters: GenerationJobListFilters, ) -> list[GenerationJob]: """Return filtered generation jobs for a user and session.""" - jobs = [ - job - for job in self.jobs.values() - if job.user_id == user_id and job.session_id == session_id + generations = [ + generation + for generation in self.generations.values() + if generation.user_id == user_id and generation.session_id == session_id ] if filters.status is not None: - jobs = [job for job in jobs if job.status == filters.status] + generations = [ + generation + for generation in generations + if generation.status == filters.status + ] if filters.search is not None: search = filters.search.strip().lower() if search: - jobs = [job for job in jobs if search in job.input_message.lower()] + generations = [ + generation + for generation in generations + if search in generation.input_message.lower() + ] - return jobs[filters.offset : filters.offset + filters.limit] + return generations[filters.offset : filters.offset + filters.limit] - async def update(self, generation_job: GenerationJob) -> GenerationJob: + async def update(self, generation: GenerationJob) -> GenerationJob: """Update generation job.""" - self.updated_job = generation_job - self.jobs[generation_job.id] = generation_job + self.updated_generation = generation + self.generations[generation.id] = generation - return generation_job + return generation def build_service( repository: FakeGenerationJobRepository, sessions: FakeChatSessionService, -) -> JobService: - """Build a JobService wired with fake dependencies.""" - return JobService( - job_repository=cast(GenerationJobRepository, repository), +) -> GenerationJobService: + """Build a GenerationJobService wired with fake dependencies.""" + return GenerationJobService( + generation_repository=cast(GenerationJobRepository, repository), session_service=cast(ChatSessionService, sessions), ) -def store_job( +def store_generation( repository: FakeGenerationJobRepository, *, job_id: UUID = TEST_JOB_ID, @@ -141,7 +153,7 @@ def store_job( status: GenerationJobStatus = GenerationJobStatus.QUEUED, ) -> GenerationJob: """Persist a generation job owned by ``TEST_USER_ID`` into the fake repository.""" - generation_job = GenerationJob( + generation = GenerationJob( id=job_id, user_id=TEST_USER_ID, session_id=session_id, @@ -149,9 +161,9 @@ def store_job( status=status, ) - repository.jobs[job_id] = generation_job + repository.generations[job_id] = generation - return generation_job + return generation @pytest.mark.asyncio @@ -164,21 +176,19 @@ async def test_create_job_success() -> None: data = GenerationJobCreateSchema(session_id=TEST_SESSION_ID, message="Hello") - generation_job = await service.create_job(TEST_USER_ID, data) + generation = await service.create_job(TEST_USER_ID, data) - assert generation_job.user_id == TEST_USER_ID - assert generation_job.session_id == TEST_SESSION_ID - assert generation_job.input_message == "Hello" - assert generation_job.status == GenerationJobStatus.QUEUED - assert repository.created_job is generation_job - assert sessions.acquired_locks == [ - (TEST_USER_ID, TEST_SESSION_ID, generation_job.id) - ] + assert generation.user_id == TEST_USER_ID + assert generation.session_id == TEST_SESSION_ID + assert generation.input_message == "Hello" + assert generation.status == GenerationJobStatus.QUEUED + assert repository.created_generation is generation + assert sessions.acquired_locks == [(TEST_USER_ID, TEST_SESSION_ID, generation.id)] @pytest.mark.asyncio async def test_create_job_session_not_owned() -> None: - """Test that creating a job for a non-owned session raises an error.""" + """Test that creating a generation for a non-owned session raises an error.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() sessions.owners[TEST_SESSION_ID] = TEST_USER_ID @@ -189,12 +199,12 @@ async def test_create_job_session_not_owned() -> None: with pytest.raises(ChatSessionNotFoundError): await service.create_job(TEST_USER_ID_2, data) - assert repository.created_job is None + assert repository.created_generation is None @pytest.mark.asyncio async def test_create_job_generation_in_progress() -> None: - """Test that creating a job raises when a generation is already in progress.""" + """Test that creating a generation raises when one is already in progress.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() sessions.owners[TEST_SESSION_ID] = TEST_USER_ID @@ -212,24 +222,24 @@ async def test_get_by_id_success() -> None: """Test successful generation job retrieval by identifier.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() - store_job(repository, input_message="Hello") + store_generation(repository, input_message="Hello") service = build_service(repository, sessions) - generation_job = await service.get_by_id(TEST_USER_ID, TEST_JOB_ID) + generation = await service.get_by_id_for_user(TEST_USER_ID, TEST_JOB_ID) - assert generation_job.id == TEST_JOB_ID - assert generation_job.input_message == "Hello" + assert generation.id == TEST_JOB_ID + assert generation.input_message == "Hello" @pytest.mark.asyncio async def test_get_by_id_not_found() -> None: - """Test that retrieval raises an error when the job is not found.""" + """Test that retrieval raises an error when the generation is not found.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() service = build_service(repository, sessions) with pytest.raises(GenerationNotFoundError): - await service.get_by_id(TEST_USER_ID, uuid4()) + await service.get_by_id_for_user(TEST_USER_ID, uuid4()) @pytest.mark.asyncio @@ -237,11 +247,11 @@ async def test_get_by_id_not_found_for_another_user() -> None: """Test that another user's generation job cannot be retrieved.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() - store_job(repository) + store_generation(repository) service = build_service(repository, sessions) with pytest.raises(GenerationNotFoundError): - await service.get_by_id(TEST_USER_ID_2, TEST_JOB_ID) + await service.get_by_id_for_user(TEST_USER_ID_2, TEST_JOB_ID) @pytest.mark.asyncio @@ -250,9 +260,9 @@ async def test_get_list_success() -> None: repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() sessions.owners[TEST_SESSION_ID] = TEST_USER_ID - store_job(repository, job_id=uuid4(), input_message="First") - store_job(repository, job_id=uuid4(), input_message="Second") - store_job( + store_generation(repository, job_id=uuid4(), input_message="First") + store_generation(repository, job_id=uuid4(), input_message="Second") + store_generation( repository, job_id=uuid4(), session_id=TEST_SESSION_ID_2, @@ -262,10 +272,13 @@ async def test_get_list_success() -> None: filters = GenerationJobListFilters(limit=10, offset=0) - jobs = await service.get_list(TEST_USER_ID, TEST_SESSION_ID, filters) + generations = await service.get_list(TEST_USER_ID, TEST_SESSION_ID, filters) - assert len(jobs) == 2 - assert {job.input_message for job in jobs} == {"First", "Second"} + assert len(generations) == 2 + assert {generation.input_message for generation in generations} == { + "First", + "Second", + } @pytest.mark.asyncio @@ -274,8 +287,8 @@ async def test_get_list_with_status_filter() -> None: repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() sessions.owners[TEST_SESSION_ID] = TEST_USER_ID - store_job(repository, job_id=uuid4(), status=GenerationJobStatus.QUEUED) - store_job(repository, job_id=uuid4(), status=GenerationJobStatus.COMPLETED) + store_generation(repository, job_id=uuid4(), status=GenerationJobStatus.QUEUED) + store_generation(repository, job_id=uuid4(), status=GenerationJobStatus.COMPLETED) service = build_service(repository, sessions) filters = GenerationJobListFilters( @@ -284,15 +297,15 @@ async def test_get_list_with_status_filter() -> None: status=GenerationJobStatus.COMPLETED, ) - jobs = await service.get_list(TEST_USER_ID, TEST_SESSION_ID, filters) + generations = await service.get_list(TEST_USER_ID, TEST_SESSION_ID, filters) - assert len(jobs) == 1 - assert jobs[0].status == GenerationJobStatus.COMPLETED + assert len(generations) == 1 + assert generations[0].status == GenerationJobStatus.COMPLETED @pytest.mark.asyncio async def test_get_list_session_not_owned() -> None: - """Test that listing jobs for a non-owned session raises an error.""" + """Test that listing generations for a non-owned session raises an error.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() service = build_service(repository, sessions) @@ -308,7 +321,7 @@ async def test_update_job_success() -> None: """Test successful generation job update across multiple fields.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() - store_job(repository, status=GenerationJobStatus.QUEUED) + store_generation(repository, status=GenerationJobStatus.QUEUED) service = build_service(repository, sessions) output_message_id = uuid4() @@ -320,12 +333,12 @@ async def test_update_job_success() -> None: finished_at=finished_at, ) - generation_job = await service.update_job(TEST_USER_ID, TEST_JOB_ID, data) + generation = await service.update_job(TEST_USER_ID, TEST_JOB_ID, data) - assert generation_job.status == GenerationJobStatus.COMPLETED - assert generation_job.output_message_id == output_message_id - assert generation_job.finished_at == finished_at - assert repository.updated_job is generation_job + assert generation.status == GenerationJobStatus.COMPLETED + assert generation.output_message_id == output_message_id + assert generation.finished_at == finished_at + assert repository.updated_generation is generation @pytest.mark.asyncio @@ -333,21 +346,23 @@ async def test_update_job_only_updates_provided_fields() -> None: """Test that update only mutates fields explicitly provided in the schema.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() - store_job(repository, input_message="Original", status=GenerationJobStatus.QUEUED) + store_generation( + repository, input_message="Original", status=GenerationJobStatus.QUEUED + ) service = build_service(repository, sessions) data = GenerationJobUpdateSchema(status=GenerationJobStatus.RUNNING) - generation_job = await service.update_job(TEST_USER_ID, TEST_JOB_ID, data) + generation = await service.update_job(TEST_USER_ID, TEST_JOB_ID, data) - assert generation_job.status == GenerationJobStatus.RUNNING - assert generation_job.input_message == "Original" - assert generation_job.output_message_id is None + assert generation.status == GenerationJobStatus.RUNNING + assert generation.input_message == "Original" + assert generation.output_message_id is None @pytest.mark.asyncio async def test_update_job_not_found() -> None: - """Test that update raises an error when the job is not found.""" + """Test that update raises an error when the generation is not found.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() service = build_service(repository, sessions) @@ -357,7 +372,7 @@ async def test_update_job_not_found() -> None: with pytest.raises(GenerationNotFoundError): await service.update_job(TEST_USER_ID, uuid4(), data) - assert repository.updated_job is None + assert repository.updated_generation is None @pytest.mark.asyncio @@ -365,7 +380,7 @@ async def test_update_job_not_found_for_another_user() -> None: """Test that another user's generation job cannot be updated.""" repository = FakeGenerationJobRepository() sessions = FakeChatSessionService() - store_job(repository) + store_generation(repository) service = build_service(repository, sessions) data = GenerationJobUpdateSchema(status=GenerationJobStatus.RUNNING) @@ -373,4 +388,161 @@ async def test_update_job_not_found_for_another_user() -> None: with pytest.raises(GenerationNotFoundError): await service.update_job(TEST_USER_ID_2, TEST_JOB_ID, data) - assert repository.updated_job is None + assert repository.updated_generation is None + + +@pytest.mark.asyncio +async def test_get_by_id_unscoped_success() -> None: + """Test successful generation job retrieval without user scoping.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + store_generation(repository, input_message="Hello") + service = build_service(repository, sessions) + + generation = await service.get_by_id(TEST_JOB_ID) + + assert generation.id == TEST_JOB_ID + assert generation.input_message == "Hello" + + +@pytest.mark.asyncio +async def test_get_by_id_unscoped_not_found() -> None: + """Test that unscoped retrieval raises when the generation is not found.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + service = build_service(repository, sessions) + + with pytest.raises(GenerationNotFoundError): + await service.get_by_id(uuid4()) + + +@pytest.mark.asyncio +async def test_set_job_running_success() -> None: + """Test that marking a job running sets the status and start time.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + store_generation(repository, status=GenerationJobStatus.QUEUED) + service = build_service(repository, sessions) + + generation = await service.set_job_running(TEST_JOB_ID) + + assert generation.status == GenerationJobStatus.RUNNING + assert generation.started_at is not None + assert repository.updated_generation is generation + + +@pytest.mark.asyncio +async def test_set_job_running_not_found() -> None: + """Test that marking a missing job running raises an error.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + service = build_service(repository, sessions) + + with pytest.raises(GenerationNotFoundError): + await service.set_job_running(uuid4()) + + assert repository.updated_generation is None + + +@pytest.mark.asyncio +async def test_set_job_failed_success() -> None: + """Test that marking a job failed sets status, error, and finish time.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + store_generation(repository, status=GenerationJobStatus.RUNNING) + service = build_service(repository, sessions) + + generation = await service.set_job_failed(TEST_JOB_ID, "boom") + + assert generation.status == GenerationJobStatus.FAILED + assert generation.error == "boom" + assert generation.finished_at is not None + assert repository.updated_generation is generation + + +@pytest.mark.asyncio +async def test_set_job_failed_truncates_error_message() -> None: + """Test that a long error message is truncated to ``ERROR_MAX_LENGTH``.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + store_generation(repository, status=GenerationJobStatus.RUNNING) + service = build_service(repository, sessions) + + error_message = "x" * (GenerationJobService.ERROR_MAX_LENGTH + 100) + + generation = await service.set_job_failed(TEST_JOB_ID, error_message) + + assert generation.error is not None + assert len(generation.error) == GenerationJobService.ERROR_MAX_LENGTH + + +@pytest.mark.asyncio +async def test_set_job_failed_not_found() -> None: + """Test that marking a missing job failed raises an error.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + service = build_service(repository, sessions) + + with pytest.raises(GenerationNotFoundError): + await service.set_job_failed(uuid4(), "boom") + + assert repository.updated_generation is None + + +@pytest.mark.asyncio +async def test_set_job_completed_success() -> None: + """Test that marking a job completed sets status, output, and finish time.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + store_generation(repository, status=GenerationJobStatus.RUNNING) + service = build_service(repository, sessions) + + message_id = uuid4() + + generation = await service.set_job_completed(TEST_JOB_ID, message_id) + + assert generation.status == GenerationJobStatus.COMPLETED + assert generation.output_message_id == message_id + assert generation.finished_at is not None + assert repository.updated_generation is generation + + +@pytest.mark.asyncio +async def test_set_job_completed_not_found() -> None: + """Test that marking a missing job completed raises an error.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + service = build_service(repository, sessions) + + with pytest.raises(GenerationNotFoundError): + await service.set_job_completed(uuid4(), uuid4()) + + assert repository.updated_generation is None + + +@pytest.mark.asyncio +async def test_set_job_cancelled_success() -> None: + """Test that marking a job cancelled sets the status and finish time.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + store_generation(repository, status=GenerationJobStatus.RUNNING) + service = build_service(repository, sessions) + + generation = await service.set_job_cancelled(TEST_JOB_ID) + + assert generation.status == GenerationJobStatus.CANCELLED + assert generation.finished_at is not None + assert repository.updated_generation is generation + + +@pytest.mark.asyncio +async def test_set_job_cancelled_not_found() -> None: + """Test that marking a missing job cancelled raises an error.""" + repository = FakeGenerationJobRepository() + sessions = FakeChatSessionService() + service = build_service(repository, sessions) + + with pytest.raises(GenerationNotFoundError): + await service.set_job_cancelled(uuid4()) + + assert repository.updated_generation is None From 53d63b733cbb07a39769a3b0987183d048d811be Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:03:04 +0300 Subject: [PATCH 66/81] feat(services): add document processing job service --- src/ai_notes_api/services/__init__.py | 6 +- .../services/document_processing_job.py | 133 +++++++++++ src/ai_notes_api/workers/tasks/processing.py | 76 ++----- .../test_document_processing_job_service.py | 207 ++++++++++++++++++ 4 files changed, 365 insertions(+), 57 deletions(-) create mode 100644 src/ai_notes_api/services/document_processing_job.py create mode 100644 tests/services/test_document_processing_job_service.py diff --git a/src/ai_notes_api/services/__init__.py b/src/ai_notes_api/services/__init__.py index a4fc662..1596f9e 100644 --- a/src/ai_notes_api/services/__init__.py +++ b/src/ai_notes_api/services/__init__.py @@ -8,7 +8,8 @@ from ai_notes_api.services.chat_session import ChatSessionService from ai_notes_api.services.document import DocumentService from ai_notes_api.services.document_processing import DocumentProcessingService -from ai_notes_api.services.generation_job import JobService +from ai_notes_api.services.document_processing_job import DocumentProcessingJobService +from ai_notes_api.services.generation_job import GenerationJobService from ai_notes_api.services.llm_service import LLMService from ai_notes_api.services.message import MessageService from ai_notes_api.services.note import NoteService @@ -16,11 +17,12 @@ __all__ = [ "AuthService", "ChatSessionService", - "JobService", + "GenerationJobService", "MessageService", "NoteService", "LLMService", "ChatMemoryService", "DocumentService", "DocumentProcessingService", + "DocumentProcessingJobService", ] diff --git a/src/ai_notes_api/services/document_processing_job.py b/src/ai_notes_api/services/document_processing_job.py new file mode 100644 index 0000000..936015a --- /dev/null +++ b/src/ai_notes_api/services/document_processing_job.py @@ -0,0 +1,133 @@ +"""Document processing job service module. + +This module provides business logic for managing document processing jobs: +enqueuing new jobs, tracking their lifecycle, and recording their terminal +status as the worker processes the associated document. +""" + +from datetime import UTC, datetime +from uuid import UUID + +from ai_notes_api.db.models import DocumentProcessingJob, DocumentProcessingJobStatus +from ai_notes_api.exceptions import DocumentProcessingJobNotFoundError +from ai_notes_api.repositories import DocumentProcessingJobRepository + + +class DocumentProcessingJobService: + """Service for document-processing-job-related business operations. + + Args: + processing_repository (DocumentProcessingJobRepository): Repository used + to perform document processing job database operations. + """ + + ERROR_MAX_LENGTH = 10_000 + + def __init__( + self, + processing_repository: DocumentProcessingJobRepository, + ) -> None: + """Initialize the document processing job service. + + Args: + processing_repository (DocumentProcessingJobRepository): Document + processing job repository used by the service. + """ + self.processing_jobs = processing_repository + + async def create_job(self, document_id: UUID) -> DocumentProcessingJob: + """Create a queued document processing job for a document. + + Args: + document_id (UUID): Unique identifier of the document to process. + + Returns: + DocumentProcessingJob: Created document processing job. + """ + processing_job = DocumentProcessingJob( + document_id=document_id, + status=DocumentProcessingJobStatus.QUEUED, + ) + + return await self.processing_jobs.create(processing_job) + + async def get_by_id(self, job_id: UUID) -> DocumentProcessingJob: + """Return a document processing job by its identifier. + + Args: + job_id (UUID): Unique document processing job identifier. + + Returns: + DocumentProcessingJob: Matching document processing job. + + Raises: + DocumentProcessingJobNotFoundError: If no document processing job with + the given identifier exists. + """ + processing_job = await self.processing_jobs.get_by_id(job_id) + + if processing_job is None: + raise DocumentProcessingJobNotFoundError() + + return processing_job + + async def set_job_running(self, job_id: UUID) -> DocumentProcessingJob: + """Mark a document processing job as running and record its start time. + + Args: + job_id (UUID): Unique document processing job identifier. + + Returns: + DocumentProcessingJob: Updated document processing job. + + Raises: + DocumentProcessingJobNotFoundError: If no document processing job exists. + """ + processing_job = await self.get_by_id(job_id) + + processing_job.status = DocumentProcessingJobStatus.RUNNING + processing_job.started_at = datetime.now(UTC) + + return await self.processing_jobs.update(processing_job) + + async def set_job_failed( + self, job_id: UUID, error_message: str | None = None + ) -> DocumentProcessingJob: + """Mark a document processing job as failed, record the error and finish time. + + Args: + job_id (UUID): Unique document processing job identifier. + error_message (str | None): Error message describing the failure. + + Returns: + DocumentProcessingJob: Updated document processing job. + + Raises: + DocumentProcessingJobNotFoundError: If no document processing job exists. + """ + processing_job = await self.get_by_id(job_id) + + processing_job.status = DocumentProcessingJobStatus.FAILED + processing_job.error = error_message[: self.ERROR_MAX_LENGTH] + processing_job.finished_at = datetime.now(UTC) + + return await self.processing_jobs.update(processing_job) + + async def set_job_completed(self, job_id: UUID) -> DocumentProcessingJob: + """Mark a document processing job as completed and record its finish time. + + Args: + job_id (UUID): Unique document processing job identifier. + + Returns: + DocumentProcessingJob: Updated document processing job. + + Raises: + DocumentProcessingJobNotFoundError: If no document processing job exists. + """ + processing_job = await self.get_by_id(job_id) + + processing_job.status = DocumentProcessingJobStatus.COMPLETED + processing_job.finished_at = datetime.now(UTC) + + return await self.processing_jobs.update(processing_job) diff --git a/src/ai_notes_api/workers/tasks/processing.py b/src/ai_notes_api/workers/tasks/processing.py index 0a0aaec..82fdb3f 100644 --- a/src/ai_notes_api/workers/tasks/processing.py +++ b/src/ai_notes_api/workers/tasks/processing.py @@ -5,17 +5,12 @@ import asyncio from contextlib import asynccontextmanager -from datetime import UTC, datetime from uuid import UUID from loguru import logger -from sqlalchemy.ext.asyncio import AsyncSession -from ai_notes_api.db.models import DocumentProcessingJobStatus from ai_notes_api.db.session import worker_session -from ai_notes_api.exceptions.document_processing_job import ( - DocumentProcessingJobNotFoundError, -) +from ai_notes_api.ingestion import TextExtractor, TokenTextChunker from ai_notes_api.integrations import openai_client from ai_notes_api.llm import EmbeddingClient from ai_notes_api.repositories import ( @@ -23,7 +18,10 @@ DocumentProcessingJobRepository, DocumentRepository, ) -from ai_notes_api.services import DocumentProcessingService +from ai_notes_api.services import ( + DocumentProcessingJobService, + DocumentProcessingService, +) from ai_notes_api.storage import DocumentStorage, get_s3_client from ai_notes_api.workers.celery_app import celery_app @@ -56,36 +54,36 @@ async def _run_document_processing_job(job_id: UUID) -> None: worker_session() as session, asynccontextmanager(get_s3_client)() as s3_client, ): - processing_job_repository = DocumentProcessingJobRepository(session) + processing_repository = DocumentProcessingJobRepository(session) document_repository = DocumentRepository(session) chunk_repository = DocumentChunkRepository(session) + processing_jobs = DocumentProcessingJobService(processing_repository) + storage = DocumentStorage(s3_client) - processing_job = await processing_job_repository.get_by_id(job_id) + text_extractor = TextExtractor() + chunker = TokenTextChunker() - if processing_job is None: - raise DocumentProcessingJobNotFoundError() + processing_job = await processing_jobs.get_by_id(job_id) document_processing = DocumentProcessingService( document_repository=document_repository, chunk_repository=chunk_repository, storage=storage, embeddings=embeddings, + text_extractor=text_extractor, + chunker=chunker, ) try: logger.info("Document processing job started: id={}", job_id) - processing_job.status = DocumentProcessingJobStatus.RUNNING - processing_job.started_at = datetime.now(UTC) - processing_job = await processing_job_repository.update(processing_job) + await processing_jobs.set_job_running(processing_job.id) - document_processing.process_document(processing_job.document_id) + await document_processing.process_document(processing_job.document_id) - processing_job.status = DocumentProcessingJobStatus.COMPLETED - processing_job.finished_at = datetime.now(UTC) - await processing_job_repository.update(processing_job) + await processing_jobs.set_job_completed(processing_job.id) await session.commit() @@ -96,43 +94,11 @@ async def _run_document_processing_job(job_id: UUID) -> None: logger.exception("Document processing job failed: id={}", job_id) - await _mark_job_failed( - session=session, - job_repository=processing_job_repository, - job_id=job_id, - error=str(exc), + await processing_jobs.set_job_failed( + job_id=processing_job.id, + error_message=str(exc), ) - raise - - -async def _mark_job_failed( - session: AsyncSession, - job_repository: DocumentProcessingJobRepository, - job_id: UUID, - error: str, -) -> None: - """Mark a document processing job as failed. - - This is invoked after the job transaction has been rolled back, so it runs - in a fresh transaction to persist the failure state. - - Args: - session (AsyncSession): Database session used to commit the failure state. - job_repository (DocumentProcessingJobRepository): Repository used to - update the processing job. - job_id (UUID): Unique document processing job identifier. - error (str): Error message describing the failure. - """ - processing_job = await job_repository.get_by_id(job_id) - - if processing_job is None: - return - - processing_job.status = DocumentProcessingJobStatus.FAILED - processing_job.error = error[:ERROR_MAX_LENGTH] - processing_job.finished_at = datetime.now(UTC) - - await job_repository.update(processing_job) + await session.commit() - await session.commit() + raise diff --git a/tests/services/test_document_processing_job_service.py b/tests/services/test_document_processing_job_service.py new file mode 100644 index 0000000..4ba6cee --- /dev/null +++ b/tests/services/test_document_processing_job_service.py @@ -0,0 +1,207 @@ +"""Tests for document processing job service.""" + +from typing import cast +from uuid import UUID, uuid4 + +import pytest + +from ai_notes_api.db.models import ( + DocumentProcessingJob, + DocumentProcessingJobStatus, +) +from ai_notes_api.exceptions import DocumentProcessingJobNotFoundError +from ai_notes_api.repositories.document_processing_job import ( + DocumentProcessingJobRepository, +) +from ai_notes_api.services.document_processing_job import DocumentProcessingJobService + +TEST_DOCUMENT_ID = UUID("11111111-1111-1111-1111-111111111111") +TEST_JOB_ID = UUID("55555555-5555-5555-5555-555555555555") + + +class FakeDocumentProcessingJobRepository: + """Fake document processing job repository used for testing service behavior.""" + + def __init__(self) -> None: + """Initialize the fake repository.""" + self.processing_jobs: dict[UUID, DocumentProcessingJob] = {} + self.created_job: DocumentProcessingJob | None = None + self.updated_job: DocumentProcessingJob | None = None + + async def create(self, job: DocumentProcessingJob) -> DocumentProcessingJob: + """Create a document processing job.""" + job.id = TEST_JOB_ID + + self.created_job = job + self.processing_jobs[job.id] = job + + return job + + async def get_by_id(self, job_id: UUID) -> DocumentProcessingJob | None: + """Return a document processing job by its identifier.""" + return self.processing_jobs.get(job_id) + + async def update(self, job: DocumentProcessingJob) -> DocumentProcessingJob: + """Update a document processing job.""" + self.updated_job = job + self.processing_jobs[job.id] = job + + return job + + +def build_service( + repository: FakeDocumentProcessingJobRepository, +) -> DocumentProcessingJobService: + """Build a DocumentProcessingJobService wired with a fake repository.""" + return DocumentProcessingJobService( + processing_repository=cast(DocumentProcessingJobRepository, repository), + ) + + +def store_job( + repository: FakeDocumentProcessingJobRepository, + *, + job_id: UUID = TEST_JOB_ID, + document_id: UUID = TEST_DOCUMENT_ID, + status: DocumentProcessingJobStatus = DocumentProcessingJobStatus.QUEUED, +) -> DocumentProcessingJob: + """Persist a document processing job into the fake repository.""" + job = DocumentProcessingJob( + id=job_id, + document_id=document_id, + status=status, + ) + + repository.processing_jobs[job_id] = job + + return job + + +@pytest.mark.asyncio +async def test_create_job_success() -> None: + """Test successful document processing job creation.""" + repository = FakeDocumentProcessingJobRepository() + service = build_service(repository) + + job = await service.create_job(TEST_DOCUMENT_ID) + + assert job.document_id == TEST_DOCUMENT_ID + assert job.status == DocumentProcessingJobStatus.QUEUED + assert repository.created_job is job + + +@pytest.mark.asyncio +async def test_get_by_id_success() -> None: + """Test successful document processing job retrieval by identifier.""" + repository = FakeDocumentProcessingJobRepository() + store_job(repository) + service = build_service(repository) + + job = await service.get_by_id(TEST_JOB_ID) + + assert job.id == TEST_JOB_ID + assert job.document_id == TEST_DOCUMENT_ID + + +@pytest.mark.asyncio +async def test_get_by_id_not_found() -> None: + """Test that retrieval raises an error when the job is not found.""" + repository = FakeDocumentProcessingJobRepository() + service = build_service(repository) + + with pytest.raises(DocumentProcessingJobNotFoundError): + await service.get_by_id(uuid4()) + + +@pytest.mark.asyncio +async def test_set_job_running_success() -> None: + """Test that marking a job running sets the status and start time.""" + repository = FakeDocumentProcessingJobRepository() + store_job(repository, status=DocumentProcessingJobStatus.QUEUED) + service = build_service(repository) + + job = await service.set_job_running(TEST_JOB_ID) + + assert job.status == DocumentProcessingJobStatus.RUNNING + assert job.started_at is not None + assert repository.updated_job is job + + +@pytest.mark.asyncio +async def test_set_job_running_not_found() -> None: + """Test that marking a missing job running raises an error.""" + repository = FakeDocumentProcessingJobRepository() + service = build_service(repository) + + with pytest.raises(DocumentProcessingJobNotFoundError): + await service.set_job_running(uuid4()) + + assert repository.updated_job is None + + +@pytest.mark.asyncio +async def test_set_job_failed_success() -> None: + """Test that marking a job failed sets status, error, and finish time.""" + repository = FakeDocumentProcessingJobRepository() + store_job(repository, status=DocumentProcessingJobStatus.RUNNING) + service = build_service(repository) + + job = await service.set_job_failed(TEST_JOB_ID, "boom") + + assert job.status == DocumentProcessingJobStatus.FAILED + assert job.error == "boom" + assert job.finished_at is not None + assert repository.updated_job is job + + +@pytest.mark.asyncio +async def test_set_job_failed_truncates_error_message() -> None: + """Test that a long error message is truncated to ``ERROR_MAX_LENGTH``.""" + repository = FakeDocumentProcessingJobRepository() + store_job(repository, status=DocumentProcessingJobStatus.RUNNING) + service = build_service(repository) + + error_message = "x" * (DocumentProcessingJobService.ERROR_MAX_LENGTH + 100) + + job = await service.set_job_failed(TEST_JOB_ID, error_message) + + assert job.error is not None + assert len(job.error) == DocumentProcessingJobService.ERROR_MAX_LENGTH + + +@pytest.mark.asyncio +async def test_set_job_failed_not_found() -> None: + """Test that marking a missing job failed raises an error.""" + repository = FakeDocumentProcessingJobRepository() + service = build_service(repository) + + with pytest.raises(DocumentProcessingJobNotFoundError): + await service.set_job_failed(uuid4(), "boom") + + assert repository.updated_job is None + + +@pytest.mark.asyncio +async def test_set_job_completed_success() -> None: + """Test that marking a job completed sets the status and finish time.""" + repository = FakeDocumentProcessingJobRepository() + store_job(repository, status=DocumentProcessingJobStatus.RUNNING) + service = build_service(repository) + + job = await service.set_job_completed(TEST_JOB_ID) + + assert job.status == DocumentProcessingJobStatus.COMPLETED + assert job.finished_at is not None + assert repository.updated_job is job + + +@pytest.mark.asyncio +async def test_set_job_completed_not_found() -> None: + """Test that marking a missing job completed raises an error.""" + repository = FakeDocumentProcessingJobRepository() + service = build_service(repository) + + with pytest.raises(DocumentProcessingJobNotFoundError): + await service.set_job_completed(uuid4()) + + assert repository.updated_job is None From db284b57173b2d3a9cc2d13ab70350beeebab624 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:03:13 +0300 Subject: [PATCH 67/81] refactor(documents): simplify document service and schemas --- src/ai_notes_api/schemas/__init__.py | 12 +- src/ai_notes_api/schemas/document.py | 56 +-- .../schemas/document_processing_job.py | 37 -- src/ai_notes_api/services/document.py | 103 ++-- tests/services/test_document_service.py | 459 ++++++++++++++++++ 5 files changed, 514 insertions(+), 153 deletions(-) delete mode 100644 src/ai_notes_api/schemas/document_processing_job.py create mode 100644 tests/services/test_document_service.py diff --git a/src/ai_notes_api/schemas/__init__.py b/src/ai_notes_api/schemas/__init__.py index 3de02df..2b18294 100644 --- a/src/ai_notes_api/schemas/__init__.py +++ b/src/ai_notes_api/schemas/__init__.py @@ -14,14 +14,10 @@ from ai_notes_api.schemas.chunk import DocumentChunkRead from ai_notes_api.schemas.completion import ChatCompletionResponseSchema from ai_notes_api.schemas.document import ( - DocumentDeleteResponse, DocumentDownloadUrlResponse, DocumentListResponse, - DocumentProcessResponse, - DocumentRead, - DocumentUploadResponse, + DocumentResponseSchema, ) -from ai_notes_api.schemas.document_processing_job import DocumentProcessingJobRead from ai_notes_api.schemas.error import ErrorResponseSchema from ai_notes_api.schemas.generation_job import ( GenerationJobCreateSchema, @@ -79,14 +75,10 @@ "GenerationJobResponseSchema", "GenerationJobUpdateSchema", "ChatMemoryResponseSchema", - "DocumentRead", + "DocumentResponseSchema", "DocumentListResponse", - "DocumentUploadResponse", - "DocumentProcessResponse", - "DocumentDeleteResponse", "DocumentDownloadUrlResponse", "DocumentChunkRead", - "DocumentProcessingJobRead", "RagQueryRequest", "RagSourceRead", "RagQueryResponse", diff --git a/src/ai_notes_api/schemas/document.py b/src/ai_notes_api/schemas/document.py index bee5ec2..7890ab7 100644 --- a/src/ai_notes_api/schemas/document.py +++ b/src/ai_notes_api/schemas/document.py @@ -12,7 +12,7 @@ from ai_notes_api.db.models import DocumentStatus -class DocumentRead(BaseModel): +class DocumentResponseSchema(BaseModel): """Schema for returning document data. Attributes: @@ -51,65 +51,13 @@ class DocumentListResponse(BaseModel): Attributes: items (list[DocumentRead]): List of documents. - limit (int): Maximum number of documents returned. - offset (int): Number of documents skipped before returning results. total (int): Total number of documents in the current page. """ - items: list[DocumentRead] - limit: int - offset: int + items: list[DocumentResponseSchema] total: int -class DocumentUploadResponse(BaseModel): - """Schema for returning the result of a document upload. - - Attributes: - id (UUID): Unique document identifier. - chat_session_id (UUID): Unique chat session identifier. - filename (str): Original document file name. - status (DocumentStatus): Current document processing status. - created_at (datetime): Date and time when the document was created. - """ - - model_config = ConfigDict(from_attributes=True, populate_by_name=True) - - id: UUID - chat_session_id: UUID = Field(validation_alias="session_id") - filename: str - status: DocumentStatus - created_at: datetime - - -class DocumentProcessResponse(BaseModel): - """Schema for returning the result of a document processing request. - - Attributes: - document_id (UUID): Unique document identifier. - status (DocumentStatus): Current document processing status. - message (str): Human-readable description of the processing result. - """ - - document_id: UUID - status: DocumentStatus - message: str - - -class DocumentDeleteResponse(BaseModel): - """Schema for returning the result of a document deletion. - - Attributes: - document_id (UUID): Unique document identifier. - status (DocumentStatus): Current document status. - message (str): Human-readable description of the deletion result. - """ - - document_id: UUID - status: DocumentStatus - message: str - - class DocumentDownloadUrlResponse(BaseModel): """Schema for returning a presigned document download URL. diff --git a/src/ai_notes_api/schemas/document_processing_job.py b/src/ai_notes_api/schemas/document_processing_job.py deleted file mode 100644 index d7015a1..0000000 --- a/src/ai_notes_api/schemas/document_processing_job.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Document processing job schemas module. - -This module defines Pydantic schemas used for document processing job API -responses. -""" - -from datetime import datetime -from uuid import UUID - -from pydantic import BaseModel, ConfigDict - -from ai_notes_api.db.models import DocumentProcessingJobStatus - - -class DocumentProcessingJobRead(BaseModel): - """Schema for returning document processing job data. - - Attributes: - id (UUID): Unique processing job identifier. - document_id (UUID): Unique document identifier. - status (DocumentProcessingJobStatus): Current processing job status. - created_at (datetime): Date and time when the processing job was created. - started_at (datetime | None): Date and time when processing started. - finished_at (datetime | None): Date and time when processing finished. - error (str | None): Optional error message if processing failed. - """ - - model_config = ConfigDict(from_attributes=True) - - id: UUID - document_id: UUID - status: DocumentProcessingJobStatus - - created_at: datetime - started_at: datetime | None = None - finished_at: datetime | None = None - error: str | None = None diff --git a/src/ai_notes_api/services/document.py b/src/ai_notes_api/services/document.py index c12c799..2df6c13 100644 --- a/src/ai_notes_api/services/document.py +++ b/src/ai_notes_api/services/document.py @@ -8,20 +8,11 @@ from fastapi import UploadFile -from ai_notes_api.db.models import ( - Document, - DocumentProcessingJob, - DocumentProcessingJobStatus, - DocumentStatus, -) +from ai_notes_api.db.models import Document, DocumentStatus from ai_notes_api.exceptions import DocumentNotFoundError -from ai_notes_api.repositories import ( - DocumentProcessingJobRepository, - DocumentRepository, -) +from ai_notes_api.repositories import DocumentRepository from ai_notes_api.services.chat_session import ChatSessionService from ai_notes_api.storage import DocumentStorage -from ai_notes_api.workers.tasks.processing import run_document_processing_job class DocumentService: @@ -30,8 +21,6 @@ class DocumentService: Args: document_repository (DocumentRepository): Repository used to perform document database operations. - processing_repository (DocumentProcessingJobRepository): Repository used - to create document processing jobs. session_service (ChatSessionService): Chat session service used to validate chat session access. storage (DocumentStorage): Object storage helper used to manage document files. @@ -43,7 +32,6 @@ class DocumentService: def __init__( self, document_repository: DocumentRepository, - processing_repository: DocumentProcessingJobRepository, session_service: ChatSessionService, storage: DocumentStorage, ) -> None: @@ -52,32 +40,25 @@ def __init__( Args: document_repository (DocumentRepository): Document repository used by the service. - processing_repository (DocumentProcessingJobRepository): Document - processing job repository used by the service. session_service (ChatSessionService): Chat session service used by the service. storage (DocumentStorage): Object storage helper used by the service. """ self.documents = document_repository self.sessions = session_service - self.processing = processing_repository self.storage = storage async def create_document( self, user_id: UUID, - chat_session_id: UUID, + session_id: UUID, file: UploadFile, ) -> Document: """Upload a file and create a document for a chat session. - Reads the uploaded file, stores it in object storage, persists a - document record in the ``UPLOADED`` status, and enqueues a processing - job for it. - Args: user_id (UUID): Unique identifier of the user uploading the document. - chat_session_id (UUID): Unique chat session identifier. + session_id (UUID): Unique chat session identifier. file (UploadFile): Uploaded file to store as a document. Returns: @@ -86,7 +67,7 @@ async def create_document( Raises: ChatSessionNotFoundError: If no accessible chat session exists. """ - await self.sessions.ensure_session_owner(user_id, chat_session_id) + await self.sessions.ensure_session_owner(user_id, session_id) data = await file.read() @@ -106,7 +87,7 @@ async def create_document( document = Document( id=document_id, user_id=user_id, - session_id=chat_session_id, + session_id=session_id, filename=filename, content_type=content_type, file_size=len(data), @@ -116,46 +97,35 @@ async def create_document( status=DocumentStatus.UPLOADED, ) - document = await self.documents.create(document) - - processing_job = await self.processing.create( - DocumentProcessingJob( - document_id=document_id, - status=DocumentProcessingJobStatus.QUEUED, - ) - ) - - run_document_processing_job.delay(str(processing_job.id)) + return await self.documents.create(document) - return document - - async def list_chat_documents( + async def list_documents( self, user_id: UUID, - chat_session_id: UUID, + session_id: UUID, ) -> list[Document]: """Return a user's documents for a chat session. Args: user_id (UUID): Unique identifier of the user who owns the documents. - chat_session_id (UUID): Unique chat session identifier. + session_id (UUID): Unique chat session identifier. Returns: list[Document]: List of the user's documents in the chat session. """ - return await self.documents.get_list_for_session(user_id, chat_session_id) + return await self.documents.get_list_for_session(user_id, session_id) - async def get_chat_document( + async def get_document( self, user_id: UUID, - chat_session_id: UUID, + session_id: UUID, document_id: UUID, ) -> Document: """Return a user's document from a chat session by its identifier. Args: user_id (UUID): Unique identifier of the user who owns the document. - chat_session_id (UUID): Unique chat session identifier. + session_id (UUID): Unique chat session identifier. document_id (UUID): Unique document identifier. Returns: @@ -166,33 +136,62 @@ async def get_chat_document( """ document = await self.documents.get_by_id_for_user(user_id, document_id) - if document is None or document.session_id != chat_session_id: + if document is None or document.session_id != session_id: raise DocumentNotFoundError() return document + async def get_document_download_url( + self, + user_id: UUID, + session_id: UUID, + document_id: UUID, + expires_in_seconds: int | None = None, + ) -> str: + """Return a presigned download URL for a user's document. + + Args: + user_id (UUID): Unique identifier of the user who owns the document. + session_id (UUID): Unique chat session identifier. + document_id (UUID): Unique document identifier. + expires_in_seconds (int | None): Number of seconds until the + presigned URL expires. If None, the storage default is used. + + Returns: + str: Presigned URL used to download the document. + + Raises: + DocumentNotFoundError: If no accessible document exists in the chat session. + """ + document = await self.documents.get_by_id_for_user(user_id, document_id) + + if document is None or document.session_id != session_id: + raise DocumentNotFoundError() + + return await self.storage.get_presigned_download_url( + object_name=document.storage_object_name, + expires_in_seconds=expires_in_seconds, + ) + async def delete_document( self, user_id: UUID, - chat_session_id: UUID, + session_id: UUID, document_id: UUID, ) -> None: """Delete a user's document from a chat session. - Soft-deletes the document and its chunks, then removes the stored file - from object storage. - Args: user_id (UUID): Unique identifier of the user who owns the document. - chat_session_id (UUID): Unique chat session identifier. + session_id (UUID): Unique chat session identifier. document_id (UUID): Unique document identifier to delete. Raises: DocumentNotFoundError: If no accessible document exists in the chat session. """ - document = await self.get_chat_document( + document = await self.get_document( user_id, - chat_session_id, + session_id, document_id, ) diff --git a/tests/services/test_document_service.py b/tests/services/test_document_service.py new file mode 100644 index 0000000..02a2888 --- /dev/null +++ b/tests/services/test_document_service.py @@ -0,0 +1,459 @@ +"""Tests for document service.""" + +import hashlib +from datetime import UTC, datetime +from typing import cast +from uuid import UUID, uuid4 + +import pytest +from fastapi import UploadFile + +from ai_notes_api.db.models import Document, DocumentStatus +from ai_notes_api.exceptions import ChatSessionNotFoundError, DocumentNotFoundError +from ai_notes_api.repositories.document import DocumentRepository +from ai_notes_api.services import ChatSessionService +from ai_notes_api.services.document import DocumentService +from ai_notes_api.storage import DocumentStorage + +TEST_USER_ID = UUID("11111111-1111-1111-1111-111111111111") +TEST_USER_ID_2 = UUID("44444444-4444-4444-4444-444444444444") +TEST_SESSION_ID = UUID("22222222-2222-2222-2222-222222222222") +TEST_SESSION_ID_2 = UUID("33333333-3333-3333-3333-333333333333") +TEST_DOCUMENT_ID = UUID("55555555-5555-5555-5555-555555555555") + +TEST_OBJECT_NAME = "users/test/document.txt" +TEST_DOWNLOAD_URL = "https://storage.example.com/download" + + +class FakeUploadFile: + """Fake uploaded file used for testing document service behavior.""" + + def __init__( + self, + *, + data: bytes = b"file content", + filename: str | None = "document.txt", + content_type: str | None = "text/plain", + ) -> None: + """Initialize the fake uploaded file.""" + self._data = data + self.filename = filename + self.content_type = content_type + + async def read(self) -> bytes: + """Return the in-memory file content.""" + return self._data + + +class FakeChatSessionService: + """Fake chat session service used for testing document service behavior.""" + + def __init__(self) -> None: + """Initialize the fake chat session service.""" + # Maps session id to the user that owns it. + self.owners: dict[UUID, UUID] = {} + + async def ensure_session_owner(self, user_id: UUID, session_id: UUID) -> None: + """Ensure a chat session belongs to a user.""" + if self.owners.get(session_id) != user_id: + raise ChatSessionNotFoundError() + + +class FakeDocumentRepository: + """Fake document repository used for testing document service behavior.""" + + def __init__(self) -> None: + """Initialize the fake document repository.""" + self.documents: dict[UUID, Document] = {} + self.created_document: Document | None = None + + async def create(self, document: Document) -> Document: + """Create a document in the fake repository.""" + self.created_document = document + self.documents[document.id] = document + return document + + async def get_by_id_for_user( + self, + user_id: UUID, + document_id: UUID, + ) -> Document | None: + """Return a non-deleted document scoped to the owning user.""" + document = self.documents.get(document_id) + + if ( + document is not None + and document.user_id == user_id + and document.deleted_at is None + ): + return document + + return None + + async def get_list_for_session( + self, + user_id: UUID, + session_id: UUID, + ) -> list[Document]: + """Return a user's non-deleted documents for a chat session.""" + return [ + document + for document in self.documents.values() + if document.user_id == user_id + and document.session_id == session_id + and document.deleted_at is None + ] + + async def soft_delete(self, document: Document) -> None: + """Soft-delete a document in the fake repository.""" + document.deleted_at = datetime.now(UTC) + + +class FakeDocumentStorage: + """Fake document storage used for testing document service behavior.""" + + def __init__(self) -> None: + """Initialize the fake document storage.""" + self.bucket = "test-bucket" + self.uploaded: list[dict[str, object]] = [] + self.presigned_calls: list[dict[str, object]] = [] + self.deleted: list[str] = [] + + async def upload_file( + self, + user_id: UUID, + document_id: UUID, + filename: str, + data: bytes, + content_type: str, + ) -> str: + """Record an upload and return a fixed object name.""" + self.uploaded.append( + { + "user_id": user_id, + "document_id": document_id, + "filename": filename, + "data": data, + "content_type": content_type, + } + ) + return TEST_OBJECT_NAME + + async def get_presigned_download_url( + self, + object_name: str, + expires_in_seconds: int | None = None, + ) -> str: + """Record a presigned URL request and return a fixed URL.""" + self.presigned_calls.append( + {"object_name": object_name, "expires_in_seconds": expires_in_seconds} + ) + return TEST_DOWNLOAD_URL + + async def delete_file(self, object_name: str) -> None: + """Record a delete request.""" + self.deleted.append(object_name) + + +def build_service( + documents: FakeDocumentRepository, + sessions: FakeChatSessionService, + storage: FakeDocumentStorage, +) -> DocumentService: + """Build a DocumentService wired with fake dependencies.""" + return DocumentService( + document_repository=cast(DocumentRepository, documents), + session_service=cast(ChatSessionService, sessions), + storage=cast(DocumentStorage, storage), + ) + + +def store_document( + repository: FakeDocumentRepository, + *, + document_id: UUID = TEST_DOCUMENT_ID, + user_id: UUID = TEST_USER_ID, + session_id: UUID = TEST_SESSION_ID, + object_name: str = TEST_OBJECT_NAME, +) -> Document: + """Persist a document into the fake repository.""" + document = Document( + id=document_id, + user_id=user_id, + session_id=session_id, + filename="document.txt", + content_type="text/plain", + file_size=12, + checksum_sha256="checksum", + storage_bucket="test-bucket", + storage_object_name=object_name, + status=DocumentStatus.READY, + ) + + repository.documents[document_id] = document + + return document + + +@pytest.mark.asyncio +async def test_create_document_success() -> None: + """Test successful document creation.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + sessions.owners[TEST_SESSION_ID] = TEST_USER_ID + service = build_service(documents, sessions, storage) + + data = b"hello world" + file = FakeUploadFile(data=data, filename="notes.txt", content_type="text/plain") + + document = await service.create_document( + TEST_USER_ID, TEST_SESSION_ID, cast(UploadFile, file) + ) + + assert document.user_id == TEST_USER_ID + assert document.session_id == TEST_SESSION_ID + assert document.filename == "notes.txt" + assert document.content_type == "text/plain" + assert document.file_size == len(data) + assert document.checksum_sha256 == hashlib.sha256(data).hexdigest() + assert document.storage_bucket == storage.bucket + assert document.storage_object_name == TEST_OBJECT_NAME + assert document.status == DocumentStatus.UPLOADED + assert documents.created_document is document + + +@pytest.mark.asyncio +async def test_create_document_uploads_file() -> None: + """Test that document creation uploads the file to storage.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + sessions.owners[TEST_SESSION_ID] = TEST_USER_ID + service = build_service(documents, sessions, storage) + + data = b"hello world" + file = FakeUploadFile(data=data, filename="notes.txt", content_type="text/plain") + + document = await service.create_document( + TEST_USER_ID, TEST_SESSION_ID, cast(UploadFile, file) + ) + + assert len(storage.uploaded) == 1 + + upload = storage.uploaded[0] + + assert upload["user_id"] == TEST_USER_ID + assert upload["document_id"] == document.id + assert upload["filename"] == "notes.txt" + assert upload["data"] == data + assert upload["content_type"] == "text/plain" + + +@pytest.mark.asyncio +async def test_create_document_uses_default_filename_and_content_type() -> None: + """Test that document creation falls back to default filename and type.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + sessions.owners[TEST_SESSION_ID] = TEST_USER_ID + service = build_service(documents, sessions, storage) + + file = FakeUploadFile(filename=None, content_type=None) + + document = await service.create_document( + TEST_USER_ID, TEST_SESSION_ID, cast(UploadFile, file) + ) + + assert document.filename == DocumentService.DEFAULT_FILENAME + assert document.content_type == DocumentService.DEFAULT_CONTENT_TYPE + + +@pytest.mark.asyncio +async def test_create_document_session_not_owned() -> None: + """Test that creating a document for a non-owned session raises an error.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + sessions.owners[TEST_SESSION_ID] = TEST_USER_ID + service = build_service(documents, sessions, storage) + + with pytest.raises(ChatSessionNotFoundError): + await service.create_document( + TEST_USER_ID_2, TEST_SESSION_ID, cast(UploadFile, FakeUploadFile()) + ) + + assert documents.created_document is None + assert storage.uploaded == [] + + +@pytest.mark.asyncio +async def test_list_documents_success() -> None: + """Test successful documents list retrieval scoped to user and session.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + store_document(documents, document_id=uuid4()) + store_document(documents, document_id=uuid4()) + store_document(documents, document_id=uuid4(), session_id=TEST_SESSION_ID_2) + store_document(documents, document_id=uuid4(), user_id=TEST_USER_ID_2) + service = build_service(documents, sessions, storage) + + result = await service.list_documents(TEST_USER_ID, TEST_SESSION_ID) + + assert len(result) == 2 + assert all(document.session_id == TEST_SESSION_ID for document in result) + assert all(document.user_id == TEST_USER_ID for document in result) + + +@pytest.mark.asyncio +async def test_list_documents_empty() -> None: + """Test that listing documents returns an empty list when none exist.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + service = build_service(documents, sessions, storage) + + result = await service.list_documents(TEST_USER_ID, TEST_SESSION_ID) + + assert result == [] + + +@pytest.mark.asyncio +async def test_get_document_success() -> None: + """Test successful document retrieval by identifier.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + store_document(documents) + service = build_service(documents, sessions, storage) + + document = await service.get_document( + TEST_USER_ID, TEST_SESSION_ID, TEST_DOCUMENT_ID + ) + + assert document.id == TEST_DOCUMENT_ID + assert document.session_id == TEST_SESSION_ID + + +@pytest.mark.asyncio +async def test_get_document_not_found_by_id() -> None: + """Test that retrieval raises an error when the document is not found.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + service = build_service(documents, sessions, storage) + + with pytest.raises(DocumentNotFoundError): + await service.get_document(TEST_USER_ID, TEST_SESSION_ID, uuid4()) + + +@pytest.mark.asyncio +async def test_get_document_not_found_for_another_user() -> None: + """Test that another user's document cannot be retrieved.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + store_document(documents) + service = build_service(documents, sessions, storage) + + with pytest.raises(DocumentNotFoundError): + await service.get_document(TEST_USER_ID_2, TEST_SESSION_ID, TEST_DOCUMENT_ID) + + +@pytest.mark.asyncio +async def test_get_document_not_found_for_another_session() -> None: + """Test that a document from another session cannot be retrieved.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + store_document(documents) + service = build_service(documents, sessions, storage) + + with pytest.raises(DocumentNotFoundError): + await service.get_document(TEST_USER_ID, TEST_SESSION_ID_2, TEST_DOCUMENT_ID) + + +@pytest.mark.asyncio +async def test_get_document_download_url_success() -> None: + """Test successful presigned download URL generation.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + store_document(documents) + service = build_service(documents, sessions, storage) + + url = await service.get_document_download_url( + user_id=TEST_USER_ID, + session_id=TEST_SESSION_ID, + document_id=TEST_DOCUMENT_ID, + expires_in_seconds=60, + ) + + assert url == TEST_DOWNLOAD_URL + assert storage.presigned_calls == [ + {"object_name": TEST_OBJECT_NAME, "expires_in_seconds": 60} + ] + + +@pytest.mark.asyncio +async def test_get_document_download_url_not_found() -> None: + """Test that URL generation raises an error when the document is not found.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + service = build_service(documents, sessions, storage) + + with pytest.raises(DocumentNotFoundError): + await service.get_document_download_url( + user_id=TEST_USER_ID, + session_id=TEST_SESSION_ID, + document_id=uuid4(), + ) + + assert storage.presigned_calls == [] + + +@pytest.mark.asyncio +async def test_delete_document_success() -> None: + """Test successful document deletion.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + store_document(documents) + service = build_service(documents, sessions, storage) + + await service.delete_document(TEST_USER_ID, TEST_SESSION_ID, TEST_DOCUMENT_ID) + + assert documents.documents[TEST_DOCUMENT_ID].deleted_at is not None + assert storage.deleted == [TEST_OBJECT_NAME] + + +@pytest.mark.asyncio +async def test_delete_document_not_found() -> None: + """Test that deletion raises an error when the document is not found.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + service = build_service(documents, sessions, storage) + + with pytest.raises(DocumentNotFoundError): + await service.delete_document(TEST_USER_ID, TEST_SESSION_ID, uuid4()) + + assert storage.deleted == [] + + +@pytest.mark.asyncio +async def test_delete_document_not_found_for_another_user() -> None: + """Test that another user's document cannot be deleted.""" + documents = FakeDocumentRepository() + sessions = FakeChatSessionService() + storage = FakeDocumentStorage() + store_document(documents) + service = build_service(documents, sessions, storage) + + with pytest.raises(DocumentNotFoundError): + await service.delete_document(TEST_USER_ID_2, TEST_SESSION_ID, TEST_DOCUMENT_ID) + + assert documents.documents[TEST_DOCUMENT_ID].deleted_at is None + assert storage.deleted == [] From 37dbabc870b5cb16d8c5d25ba583400ce89c6b26 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:03:20 +0300 Subject: [PATCH 68/81] feat(documents): add documents router and wire job services --- src/ai_notes_api/api/v1/dependencies.py | 29 +- src/ai_notes_api/api/v1/documents.py | 288 ++++++++++++++++ .../{generation_jobs.py => generation_job.py} | 34 +- src/ai_notes_api/api/v1/router.py | 6 +- tests/api/test_documents_route.py | 313 ++++++++++++++++++ tests/api/test_generation_jobs_route.py | 12 +- 6 files changed, 657 insertions(+), 25 deletions(-) create mode 100644 src/ai_notes_api/api/v1/documents.py rename src/ai_notes_api/api/v1/{generation_jobs.py => generation_job.py} (74%) create mode 100644 tests/api/test_documents_route.py diff --git a/src/ai_notes_api/api/v1/dependencies.py b/src/ai_notes_api/api/v1/dependencies.py index d3dc383..e548e28 100644 --- a/src/ai_notes_api/api/v1/dependencies.py +++ b/src/ai_notes_api/api/v1/dependencies.py @@ -31,8 +31,9 @@ AuthService, ChatMemoryService, ChatSessionService, + DocumentProcessingJobService, DocumentService, - JobService, + GenerationJobService, LLMService, MessageService, NoteService, @@ -206,7 +207,7 @@ def get_llm_service( def get_job_service( session: Annotated[AsyncSession, Depends(get_db)], -) -> JobService: +) -> GenerationJobService: """Provide a generation job service instance. Args: @@ -224,8 +225,8 @@ def get_job_service( memory_repository=memories, ) - return JobService( - job_repository=jobs, + return GenerationJobService( + generation_repository=jobs, session_service=sessions_service, ) @@ -262,7 +263,6 @@ def get_document_service( DocumentService: Configured document service instance. """ documents = DocumentRepository(db_session) - processing = DocumentProcessingJobRepository(db_session) sessions = ChatSessionRepository(db_session) memories = ChatMemoryRepository(db_session) @@ -275,7 +275,24 @@ def get_document_service( return DocumentService( document_repository=documents, - processing_repository=processing, session_service=sessions_service, storage=storage, ) + + +def get_document_processinng_job_service( + session: Annotated[AsyncSession, Depends(get_db)], +) -> DocumentProcessingJobService: + """Provide a document processing job service instance. + + Args: + session (AsyncSession): Asynchronous database session provided by + FastAPI dependency injection. + + Returns: + DocumentProcessingJobService: Configured document processing job service + instance. + """ + processing = DocumentProcessingJobRepository(session) + + return DocumentProcessingJobService(processing) diff --git a/src/ai_notes_api/api/v1/documents.py b/src/ai_notes_api/api/v1/documents.py new file mode 100644 index 0000000..3e754fb --- /dev/null +++ b/src/ai_notes_api/api/v1/documents.py @@ -0,0 +1,288 @@ +"""Documents API router. + +This module defines API endpoints for uploading, reading, downloading, +listing, and deleting chat session documents. +""" + +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Depends, File, UploadFile, status +from loguru import logger + +from ai_notes_api.api.v1.dependencies import ( + get_current_user, + get_document_processinng_job_service, + get_document_service, +) +from ai_notes_api.db.models import User +from ai_notes_api.schemas import ( + DocumentDownloadUrlResponse, + DocumentListResponse, + DocumentResponseSchema, + ErrorResponseSchema, + StatusResponseSchema, +) +from ai_notes_api.services import DocumentProcessingJobService, DocumentService +from ai_notes_api.workers.tasks.processing import run_document_processing_job + +router = APIRouter( + prefix="/chat/sessions/{session_id}/documents", + tags=["Documents"], +) + + +DOCUMENT_DOWNLOAD_URL_EXPIRES = 60 + + +@router.post( + "", + summary="Upload document to chat session", + description="Upload a document and attach it to a chat session.", + response_model=DocumentResponseSchema, + status_code=status.HTTP_201_CREATED, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + 404: { + "model": ErrorResponseSchema, + "description": "Session not found", + }, + }, +) +async def upload_document( + session_id: UUID, + file: Annotated[UploadFile, File(...)], + user: Annotated[User, Depends(get_current_user)], + document_service: Annotated[DocumentService, Depends(get_document_service)], + job_service: Annotated[ + DocumentProcessingJobService, Depends(get_document_processinng_job_service) + ], +) -> DocumentResponseSchema: + """Upload a document to a chat session. + + Args: + session_id (UUID): Unique chat session identifier. + file (UploadFile): Uploaded document file. + user (User): Current authenticated user. + document_service (DocumentService): Document service dependency used to + create the document. + job_service (DocumentProcessingJobService): Processing job service + dependency used to enqueue document processing. + + Returns: + DocumentResponseSchema: Created document data. + + Raises: + ChatSessionNotFoundError: If no chat session with the given identifier exists. + """ + logger.info( + "Document upload requested: session_id={}, filename={}", + session_id, + file.filename, + ) + + document = await document_service.create_document(user.id, session_id, file) + + processing_job = await job_service.create_job(document.id) + + run_document_processing_job.delay(str(processing_job.id)) + + return DocumentResponseSchema.model_validate(document) + + +@router.get( + "", + summary="List chat session documents", + description="Return all documents attached to a chat session.", + response_model=DocumentListResponse, + status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + }, +) +async def list_documents( + session_id: UUID, + user: Annotated[User, Depends(get_current_user)], + service: Annotated[DocumentService, Depends(get_document_service)], +) -> DocumentListResponse: + """Return documents attached to a chat session. + + Args: + session_id (UUID): Unique chat session identifier. + user (User): Current authenticated user. + service (DocumentService): Document service dependency used to retrieve + documents. + + Returns: + DocumentListResponse: List of documents attached to the chat session. + """ + logger.info("Documents list requested: session_id={}", session_id) + + documents = await service.list_documents(user.id, session_id) + + return DocumentListResponse( + items=[ + DocumentResponseSchema.model_validate(document) for document in documents + ], + total=len(documents), + ) + + +@router.get( + "/{document_id}", + summary="Get document by ID", + description="Return document metadata by its unique identifier.", + response_model=DocumentResponseSchema, + status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + 404: { + "model": ErrorResponseSchema, + "description": "Document not found", + }, + }, +) +async def get_document( + session_id: UUID, + document_id: UUID, + user: Annotated[User, Depends(get_current_user)], + service: Annotated[DocumentService, Depends(get_document_service)], +) -> DocumentResponseSchema: + """Return document metadata by its identifier. + + Args: + session_id (UUID): Unique chat session identifier. + document_id (UUID): Unique document identifier. + user (User): Current authenticated user. + service (DocumentService): Document service dependency used to retrieve + the document. + + Returns: + DocumentResponseSchema: Document data. + + Raises: + DocumentNotFoundError: If no document with the given identifier exists. + """ + logger.info( + "Document retrieval requested: session_id={}, document_id={}", + session_id, + document_id, + ) + + document = await service.get_document(user.id, session_id, document_id) + + return DocumentResponseSchema.model_validate(document) + + +@router.get( + "/{document_id}/download", + summary="Download document by ID", + description="Return a presigned URL to download a document by its identifier.", + response_model=DocumentDownloadUrlResponse, + status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + 404: { + "model": ErrorResponseSchema, + "description": "Document not found", + }, + }, +) +async def download_document( + session_id: UUID, + document_id: UUID, + user: Annotated[User, Depends(get_current_user)], + service: Annotated[DocumentService, Depends(get_document_service)], +) -> DocumentDownloadUrlResponse: + """Return a presigned URL to download a document by its identifier. + + Args: + session_id (UUID): Unique chat session identifier. + document_id (UUID): Unique document identifier. + user (User): Current authenticated user. + service (DocumentService): Document service dependency used to generate + the presigned download URL. + + Returns: + DocumentDownloadUrlResponse: Presigned download URL and its expiration. + + Raises: + DocumentNotFoundError: If no document with the given identifier exists. + """ + logger.info( + "Document download requested: session_id={}, document_id={}", + session_id, + document_id, + ) + + url = await service.get_document_download_url( + user_id=user.id, + session_id=session_id, + document_id=document_id, + expires_in_seconds=DOCUMENT_DOWNLOAD_URL_EXPIRES, + ) + + return DocumentDownloadUrlResponse( + url=url, expires_in_seconds=DOCUMENT_DOWNLOAD_URL_EXPIRES + ) + + +@router.delete( + "/{document_id}", + summary="Delete document by ID", + description="Delete a document from a chat session by its unique identifier.", + response_model=StatusResponseSchema, + status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + 404: { + "model": ErrorResponseSchema, + "description": "Document not found", + }, + }, +) +async def delete_document( + session_id: UUID, + document_id: UUID, + user: Annotated[User, Depends(get_current_user)], + service: Annotated[DocumentService, Depends(get_document_service)], +) -> StatusResponseSchema: + """Delete a document by its identifier. + + Args: + session_id (UUID): Unique chat session identifier. + document_id (UUID): Unique document identifier to delete. + user (User): Current authenticated user. + service (DocumentService): Document service dependency used to delete + the document. + + Returns: + StatusResponseSchema: Response status. + + Raises: + DocumentNotFoundError: If no document with the given identifier exists. + """ + logger.info( + "Document deletion requested: session_id={}, document_id={}", + session_id, + document_id, + ) + + await service.delete_document(user.id, session_id, document_id) + + return StatusResponseSchema(status="deleted") diff --git a/src/ai_notes_api/api/v1/generation_jobs.py b/src/ai_notes_api/api/v1/generation_job.py similarity index 74% rename from src/ai_notes_api/api/v1/generation_jobs.py rename to src/ai_notes_api/api/v1/generation_job.py index 0ce106a..c80326a 100644 --- a/src/ai_notes_api/api/v1/generation_jobs.py +++ b/src/ai_notes_api/api/v1/generation_job.py @@ -17,7 +17,7 @@ GenerationJobCreateSchema, GenerationJobResponseSchema, ) -from ai_notes_api.services import JobService +from ai_notes_api.services import GenerationJobService from ai_notes_api.workers.tasks.generation import run_generation_job router = APIRouter( @@ -33,6 +33,14 @@ response_model=GenerationJobResponseSchema, status_code=status.HTTP_201_CREATED, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + 404: { + "model": ErrorResponseSchema, + "description": "Chat session not found", + }, 409: { "model": ErrorResponseSchema, "description": "Generation already in progress", @@ -42,13 +50,12 @@ async def create_completion_job( data: GenerationJobCreateSchema, user: Annotated[User, Depends(get_current_user)], - service: Annotated[JobService, Depends(get_job_service)], + service: Annotated[GenerationJobService, Depends(get_job_service)], ) -> GenerationJobResponseSchema: """Create a generation job. Args: - data (GenerationJobCreateSchema): Validated generation job creation - data. + data (GenerationJobCreateSchema): Validated generation job creation data. user (User): Current authenticated user. service (JobService): Generation job service dependency used to create the generation job. @@ -62,11 +69,11 @@ async def create_completion_job( """ logger.info("Generation job creation requested") - job = await service.create_job(user.id, data) + generation = await service.create_job(user.id, data) - run_generation_job.delay(str(job.id)) + run_generation_job.delay(str(generation.id)) - return GenerationJobResponseSchema.model_validate(job) + return GenerationJobResponseSchema.model_validate(generation) @router.get( @@ -76,6 +83,10 @@ async def create_completion_job( response_model=GenerationJobResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Generation job not found", @@ -85,7 +96,7 @@ async def create_completion_job( async def get_completion_job( job_id: UUID, user: Annotated[User, Depends(get_current_user)], - service: Annotated[JobService, Depends(get_job_service)], + service: Annotated[GenerationJobService, Depends(get_job_service)], ) -> GenerationJobResponseSchema: """Return a generation job by its identifier. @@ -99,11 +110,10 @@ async def get_completion_job( GenerationJobResponseSchema: Generation job data. Raises: - GenerationNotFoundError: If no generation job with the given identifier - exists. + GenerationNotFoundError: If no generation job with the given identifier exists. """ logger.info("Generation job retrieval requested: job_id={}", job_id) - job = await service.get_by_id(user.id, job_id) + generation = await service.get_by_id_for_user(user.id, job_id) - return GenerationJobResponseSchema.model_validate(job) + return GenerationJobResponseSchema.model_validate(generation) diff --git a/src/ai_notes_api/api/v1/router.py b/src/ai_notes_api/api/v1/router.py index 2c54677..8c63871 100644 --- a/src/ai_notes_api/api/v1/router.py +++ b/src/ai_notes_api/api/v1/router.py @@ -10,7 +10,8 @@ auth, chat_sessions, completions, - generation_jobs, + documents, + generation_job, healthcheck, messages, notes, @@ -25,5 +26,6 @@ router.include_router(notes.router) router.include_router(chat_sessions.router) router.include_router(messages.router) +router.include_router(documents.router) router.include_router(completions.router) -router.include_router(generation_jobs.router) +router.include_router(generation_job.router) diff --git a/tests/api/test_documents_route.py b/tests/api/test_documents_route.py new file mode 100644 index 0000000..806d63d --- /dev/null +++ b/tests/api/test_documents_route.py @@ -0,0 +1,313 @@ +"""Tests for documents API router.""" + +from datetime import UTC, datetime +from unittest.mock import AsyncMock, MagicMock +from uuid import UUID + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ai_notes_api.api.v1 import documents as documents_module +from ai_notes_api.api.v1.dependencies import ( + get_current_user, + get_document_processinng_job_service, + get_document_service, +) +from ai_notes_api.api.v1.documents import router +from ai_notes_api.db.models import Document, DocumentProcessingJob, DocumentStatus, User + +TEST_USER_ID = UUID("11111111-1111-1111-1111-111111111111") +TEST_SESSION_ID = UUID("22222222-2222-2222-2222-222222222222") +TEST_DOCUMENT_ID = UUID("55555555-5555-5555-5555-555555555555") +TEST_DOCUMENT_ID_2 = UUID("66666666-6666-6666-6666-666666666666") + +TEST_DOWNLOAD_URL = "https://storage.example.com/download" +TEST_PROCESSING_JOB_ID = UUID("77777777-7777-7777-7777-777777777777") + + +def create_test_user() -> User: + """Create current user for router tests. + + Returns: + User: Test user model instance. + """ + return User( + id=TEST_USER_ID, + email="test-user@example.com", + username="test_user", + hashed_password="test-password-hash", # noqa: S106 + is_active=True, + is_superuser=False, + ) + + +def create_document( # noqa: PLR0913 + *, + document_id: UUID = TEST_DOCUMENT_ID, + session_id: UUID = TEST_SESSION_ID, + filename: str = "document.txt", + content_type: str = "text/plain", + file_size: int = 12, + status: DocumentStatus = DocumentStatus.READY, +) -> Document: + """Create a document model instance for router tests. + + Args: + document_id (UUID): Unique document identifier. + session_id (UUID): Unique chat session identifier. + filename (str): Original document file name. + content_type (str): MIME type of the document. + file_size (int): Document size in bytes. + status (DocumentStatus): Current document processing status. + + Returns: + Document: Document model instance. + """ + now = datetime.now(UTC) + + return Document( + id=document_id, + user_id=TEST_USER_ID, + session_id=session_id, + filename=filename, + content_type=content_type, + file_size=file_size, + checksum_sha256="checksum", + storage_bucket="test-bucket", + storage_object_name="users/test/document.txt", + status=status, + error_message=None, + created_at=now, + updated_at=now, + ) + + +@pytest.fixture +def current_user() -> User: + """Create mocked current user. + + Returns: + User: Current authenticated user. + """ + return create_test_user() + + +@pytest.fixture +def document_service_mock() -> AsyncMock: + """Create mocked document service. + + Returns: + AsyncMock: Mocked document service dependency. + """ + return AsyncMock() + + +@pytest.fixture +def job_service_mock() -> AsyncMock: + """Create mocked document processing job service. + + Returns: + AsyncMock: Mocked document processing job service dependency. + """ + return AsyncMock() + + +@pytest.fixture +def client( + document_service_mock: AsyncMock, + job_service_mock: AsyncMock, + current_user: User, +) -> TestClient: + """Create a test client with mocked dependencies. + + Args: + document_service_mock (AsyncMock): Mocked document service dependency. + job_service_mock (AsyncMock): Mocked processing job service dependency. + current_user (User): Mocked authenticated user. + + Returns: + TestClient: FastAPI test client. + """ + app = FastAPI() + app.include_router(router) + + app.dependency_overrides[get_document_service] = lambda: document_service_mock + app.dependency_overrides[get_document_processinng_job_service] = lambda: ( + job_service_mock + ) + app.dependency_overrides[get_current_user] = lambda: current_user + + return TestClient(app) + + +def test_upload_document_success( + client: TestClient, + document_service_mock: AsyncMock, + job_service_mock: AsyncMock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test successful document upload, job creation, and processing enqueue.""" + document_service_mock.create_document.return_value = create_document( + document_id=TEST_DOCUMENT_ID, + filename="notes.txt", + ) + job_service_mock.create_job.return_value = DocumentProcessingJob( + id=TEST_PROCESSING_JOB_ID, + document_id=TEST_DOCUMENT_ID, + ) + + delay_mock = MagicMock() + monkeypatch.setattr( + documents_module.run_document_processing_job, "delay", delay_mock + ) + + response = client.post( + f"/chat/sessions/{TEST_SESSION_ID}/documents", + files={"file": ("notes.txt", b"hello world", "text/plain")}, + ) + + assert response.status_code == 201 + + data = response.json() + + assert data["id"] == str(TEST_DOCUMENT_ID) + assert data["filename"] == "notes.txt" + assert data["chat_session_id"] == str(TEST_SESSION_ID) + + document_service_mock.create_document.assert_awaited_once() + + create_args = document_service_mock.create_document.await_args.args + + assert create_args[0] == TEST_USER_ID + assert create_args[1] == TEST_SESSION_ID + + job_service_mock.create_job.assert_awaited_once_with(TEST_DOCUMENT_ID) + delay_mock.assert_called_once_with(str(TEST_PROCESSING_JOB_ID)) + + +def test_list_documents_success( + client: TestClient, + document_service_mock: AsyncMock, +) -> None: + """Test successful documents list retrieval.""" + document_service_mock.list_documents.return_value = [ + create_document(document_id=TEST_DOCUMENT_ID, filename="first.txt"), + create_document(document_id=TEST_DOCUMENT_ID_2, filename="second.txt"), + ] + + response = client.get(f"/chat/sessions/{TEST_SESSION_ID}/documents") + + assert response.status_code == 200 + + data = response.json() + + assert data["total"] == 2 + assert len(data["items"]) == 2 + assert data["items"][0]["id"] == str(TEST_DOCUMENT_ID) + assert data["items"][0]["filename"] == "first.txt" + assert data["items"][0]["chat_session_id"] == str(TEST_SESSION_ID) + assert data["items"][1]["id"] == str(TEST_DOCUMENT_ID_2) + + document_service_mock.list_documents.assert_awaited_once_with( + TEST_USER_ID, TEST_SESSION_ID + ) + + +def test_list_documents_empty_success( + client: TestClient, + document_service_mock: AsyncMock, +) -> None: + """Test successful empty documents list retrieval.""" + document_service_mock.list_documents.return_value = [] + + response = client.get(f"/chat/sessions/{TEST_SESSION_ID}/documents") + + assert response.status_code == 200 + + data = response.json() + + assert data["items"] == [] + assert data["total"] == 0 + + document_service_mock.list_documents.assert_awaited_once_with( + TEST_USER_ID, TEST_SESSION_ID + ) + + +def test_get_document_success( + client: TestClient, + document_service_mock: AsyncMock, +) -> None: + """Test successful document retrieval by identifier.""" + document_service_mock.get_document.return_value = create_document( + document_id=TEST_DOCUMENT_ID, + filename="notes.txt", + content_type="text/plain", + status=DocumentStatus.READY, + ) + + response = client.get( + f"/chat/sessions/{TEST_SESSION_ID}/documents/{TEST_DOCUMENT_ID}" + ) + + assert response.status_code == 200 + + data = response.json() + + assert data["id"] == str(TEST_DOCUMENT_ID) + assert data["chat_session_id"] == str(TEST_SESSION_ID) + assert data["filename"] == "notes.txt" + assert data["content_type"] == "text/plain" + assert data["status"] == DocumentStatus.READY.value + + document_service_mock.get_document.assert_awaited_once_with( + TEST_USER_ID, TEST_SESSION_ID, TEST_DOCUMENT_ID + ) + + +def test_download_document_success( + client: TestClient, + document_service_mock: AsyncMock, +) -> None: + """Test successful document download URL retrieval.""" + document_service_mock.get_document_download_url.return_value = TEST_DOWNLOAD_URL + + response = client.get( + f"/chat/sessions/{TEST_SESSION_ID}/documents/{TEST_DOCUMENT_ID}/download" + ) + + assert response.status_code == 200 + + data = response.json() + + assert data["url"] == TEST_DOWNLOAD_URL + assert data["expires_in_seconds"] == 60 + + document_service_mock.get_document_download_url.assert_awaited_once() + + kwargs = document_service_mock.get_document_download_url.await_args.kwargs + + assert kwargs["user_id"] == TEST_USER_ID + assert kwargs["session_id"] == TEST_SESSION_ID + assert kwargs["document_id"] == TEST_DOCUMENT_ID + assert kwargs["expires_in_seconds"] == 60 + + +def test_delete_document_success( + client: TestClient, + document_service_mock: AsyncMock, +) -> None: + """Test successful document deletion.""" + document_service_mock.delete_document.return_value = None + + response = client.delete( + f"/chat/sessions/{TEST_SESSION_ID}/documents/{TEST_DOCUMENT_ID}" + ) + + assert response.status_code == 200 + assert response.json() == {"status": "deleted"} + + document_service_mock.delete_document.assert_awaited_once_with( + TEST_USER_ID, TEST_SESSION_ID, TEST_DOCUMENT_ID + ) diff --git a/tests/api/test_generation_jobs_route.py b/tests/api/test_generation_jobs_route.py index 77ddc3b..9807e5c 100644 --- a/tests/api/test_generation_jobs_route.py +++ b/tests/api/test_generation_jobs_route.py @@ -10,7 +10,7 @@ from fastapi.testclient import TestClient from ai_notes_api.api.v1.dependencies import get_current_user, get_job_service -from ai_notes_api.api.v1.generation_jobs import router +from ai_notes_api.api.v1.generation_job import router from ai_notes_api.db.models import GenerationJobStatus, User from ai_notes_api.exceptions import ( ChatSessionNotFoundError, @@ -93,7 +93,7 @@ def run_generation_job_mock() -> Generator[MagicMock]: Yields: MagicMock: Mocked ``run_generation_job`` Celery task. """ - with patch("ai_notes_api.api.v1.generation_jobs.run_generation_job") as task_mock: + with patch("ai_notes_api.api.v1.generation_job.run_generation_job") as task_mock: yield task_mock @@ -256,7 +256,7 @@ def test_get_completion_job_success( job_service_mock: AsyncMock, ) -> None: """Test successful generation job retrieval by identifier.""" - job_service_mock.get_by_id.return_value = create_generation_job_response( + job_service_mock.get_by_id_for_user.return_value = create_generation_job_response( status=GenerationJobStatus.COMPLETED, input_message="Hello", ) @@ -271,7 +271,9 @@ def test_get_completion_job_success( assert data["status"] == GenerationJobStatus.COMPLETED.value assert data["input_message"] == "Hello" - job_service_mock.get_by_id.assert_awaited_once_with(TEST_USER_ID, TEST_JOB_ID) + job_service_mock.get_by_id_for_user.assert_awaited_once_with( + TEST_USER_ID, TEST_JOB_ID + ) def test_get_completion_job_not_found( @@ -279,7 +281,7 @@ def test_get_completion_job_not_found( job_service_mock: AsyncMock, ) -> None: """Test that retrieving a missing generation job returns a 404 error.""" - job_service_mock.get_by_id.side_effect = GenerationNotFoundError() + job_service_mock.get_by_id_for_user.side_effect = GenerationNotFoundError() response = client.get(f"/chat/completions/jobs/{TEST_JOB_ID}") From c47187cd0b69b76047521b6d7e33bf8c39506d1e Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:03:29 +0300 Subject: [PATCH 69/81] docs(api): add error responses to endpoints --- src/ai_notes_api/api/v1/auth.py | 23 +++++++++++++++++ src/ai_notes_api/api/v1/chat_sessions.py | 32 ++++++++++++++++++++++++ src/ai_notes_api/api/v1/completions.py | 4 +++ src/ai_notes_api/api/v1/messages.py | 8 ++++++ src/ai_notes_api/api/v1/notes.py | 24 ++++++++++++++++++ 5 files changed, 91 insertions(+) diff --git a/src/ai_notes_api/api/v1/auth.py b/src/ai_notes_api/api/v1/auth.py index f34551b..cb01bb7 100644 --- a/src/ai_notes_api/api/v1/auth.py +++ b/src/ai_notes_api/api/v1/auth.py @@ -15,6 +15,7 @@ ) from ai_notes_api.db.models import User from ai_notes_api.schemas import ( + ErrorResponseSchema, TokenResponseSchema, UserCreateSchema, UserResponseSchema, @@ -33,6 +34,12 @@ description="Create a new user account and return the created user data.", response_model=UserResponseSchema, status_code=status.HTTP_201_CREATED, + responses={ + 409: { + "model": ErrorResponseSchema, + "description": "User already exists", + }, + }, ) async def register_user( data: UserCreateSchema, @@ -64,6 +71,16 @@ async def register_user( description="Authenticate a user and return an access token.", response_model=TokenResponseSchema, status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid email or password", + }, + 403: { + "model": ErrorResponseSchema, + "description": "User account is inactive", + }, + }, ) async def login_user( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], @@ -101,6 +118,12 @@ async def login_user( description="Return the currently authenticated user.", response_model=UserResponseSchema, status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + }, ) async def get_current_user_profile( user: Annotated[User, Depends(get_current_user_dependency)], diff --git a/src/ai_notes_api/api/v1/chat_sessions.py b/src/ai_notes_api/api/v1/chat_sessions.py index 2187c55..80c045a 100644 --- a/src/ai_notes_api/api/v1/chat_sessions.py +++ b/src/ai_notes_api/api/v1/chat_sessions.py @@ -43,6 +43,12 @@ description="Create a new chat session and return the created data.", response_model=ChatSessionResponseSchema, status_code=status.HTTP_201_CREATED, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + }, ) async def create_chat_session( data: ChatSessionCreateSchema, @@ -73,6 +79,12 @@ async def create_chat_session( description="Return a paginated list of chat sessions.", response_model=ChatSessionListResponseSchema, status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + }, ) async def get_chat_sessions( filters: Annotated[ @@ -120,6 +132,10 @@ async def get_chat_sessions( response_model=ChatSessionResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Chat session not found", @@ -159,6 +175,10 @@ async def get_chat_session( response_model=ChatSessionResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Chat session not found", @@ -200,6 +220,10 @@ async def update_chat_session( response_model=StatusResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Chat session not found", @@ -239,6 +263,10 @@ async def delete_chat_session( response_model=MessageListResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Chat session not found", @@ -287,6 +315,10 @@ async def get_chat_session_messages( response_model=ChatMemoryResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Chat memory not found", diff --git a/src/ai_notes_api/api/v1/completions.py b/src/ai_notes_api/api/v1/completions.py index e49eec4..a62edec 100644 --- a/src/ai_notes_api/api/v1/completions.py +++ b/src/ai_notes_api/api/v1/completions.py @@ -49,6 +49,10 @@ def llm_event_to_sse(event: LLMStreamEvent) -> dict[str, str]: description="Generate and stream an assistant response for a chat session.", status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Chat session not found", diff --git a/src/ai_notes_api/api/v1/messages.py b/src/ai_notes_api/api/v1/messages.py index 71b68e5..81c5285 100644 --- a/src/ai_notes_api/api/v1/messages.py +++ b/src/ai_notes_api/api/v1/messages.py @@ -31,6 +31,10 @@ response_model=MessageResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Message not found", @@ -70,6 +74,10 @@ async def get_message( response_model=StatusResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Message not found", diff --git a/src/ai_notes_api/api/v1/notes.py b/src/ai_notes_api/api/v1/notes.py index ed2c3e5..28e441a 100644 --- a/src/ai_notes_api/api/v1/notes.py +++ b/src/ai_notes_api/api/v1/notes.py @@ -34,6 +34,12 @@ description="Create a new note and return the created note data.", response_model=NoteResponseSchema, status_code=status.HTTP_201_CREATED, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + }, ) async def create_note( data: NoteCreateSchema, @@ -63,6 +69,12 @@ async def create_note( description="Return a paginated list of notes.", response_model=NoteListResponseSchema, status_code=status.HTTP_200_OK, + responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, + }, ) async def get_notes( filters: Annotated[ @@ -112,6 +124,10 @@ async def get_notes( response_model=NoteResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Note not found", @@ -150,6 +166,10 @@ async def get_note( response_model=NoteResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Note not found", @@ -190,6 +210,10 @@ async def update_note( response_model=StatusResponseSchema, status_code=status.HTTP_200_OK, responses={ + 401: { + "model": ErrorResponseSchema, + "description": "Invalid authentication credentials", + }, 404: { "model": ErrorResponseSchema, "description": "Note not found", From aebcf64c0b8533f734357b0403a9ef201f427efd Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:03:37 +0300 Subject: [PATCH 70/81] test(llm): add tool call and memory task tests --- tests/services/test_llm_service.py | 136 +++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 5 deletions(-) diff --git a/tests/services/test_llm_service.py b/tests/services/test_llm_service.py index 8157bb6..84b7c8a 100644 --- a/tests/services/test_llm_service.py +++ b/tests/services/test_llm_service.py @@ -1,8 +1,9 @@ """Tests for LLM service.""" -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Generator from types import SimpleNamespace from typing import Any, cast +from unittest.mock import patch from uuid import UUID import pytest @@ -11,7 +12,7 @@ from ai_notes_api.db.models import Message, MessageRole from ai_notes_api.exceptions import ChatSessionNotFoundError from ai_notes_api.llm import LLMClient -from ai_notes_api.llm.models import LLMResponse, LLMStreamEvent +from ai_notes_api.llm.schemas import LLMResponse, LLMStreamEvent, LLMToolCall from ai_notes_api.schemas import ( AssistantMessageCreateSchema, UserMessageCreateSchema, @@ -26,6 +27,20 @@ TEST_MESSAGE_ID = UUID("33333333-3333-3333-3333-333333333333") +@pytest.fixture(autouse=True) +def patch_memory_task() -> Generator[None]: + """Patch the Celery memory-summary task to avoid hitting the broker. + + Persisting an assistant message enqueues ``update_chat_memory_summary``; + without this patch the ``.delay()`` call would block on the message broker. + + Yields: + None: Control while the Celery task is patched. + """ + with patch("ai_notes_api.services.llm_service.update_chat_memory_summary"): + yield + + class FakeMessageService: """Fake message service recording calls for LLM service testing.""" @@ -139,6 +154,12 @@ def __init__(self) -> None: """Initialize the fake LLM client.""" self.response: LLMResponse | None = None self.events: list[LLMStreamEvent] = [] + # Optional queues used to return a different result per call, e.g. to + # drive the tool-calls loop across successive model invocations. + self.responses: list[LLMResponse] = [] + self.event_batches: list[list[LLMStreamEvent]] = [] + self.create_call_count = 0 + self.stream_call_count = 0 self.create_input: Any = None self.stream_input: Any = None self.create_tools: Any = None @@ -152,10 +173,15 @@ async def create_response( tools: list[dict[str, Any]] | None = None, instructions: str | None = None, ) -> LLMResponse: - """Return the configured response.""" + """Return the configured response (or the next queued one).""" + self.create_call_count += 1 self.create_input = input_data self.create_tools = tools self.create_instructions = instructions + + if self.responses: + return self.responses.pop(0) + assert self.response is not None return self.response @@ -165,11 +191,15 @@ async def stream_response_events( tools: list[dict[str, Any]] | None = None, instructions: str | None = None, ) -> AsyncGenerator[LLMStreamEvent]: - """Yield the configured stream events.""" + """Yield the configured stream events (or the next queued batch).""" + self.stream_call_count += 1 self.stream_input = input_data self.stream_tools = tools self.stream_instructions = instructions - for event in self.events: + + events = self.event_batches.pop(0) if self.event_batches else self.events + + for event in events: yield event @@ -189,6 +219,23 @@ async def get_notes_list( return self.notes +class FakeToolRegistry: + """Fake tool registry recording tool executions for LLM service testing.""" + + def __init__(self) -> None: + """Initialize the fake tool registry.""" + self.calls: list[tuple[str, str]] = [] + + def get_tools(self) -> list[dict[str, Any]]: + """Return an empty tool schema list.""" + return [] + + async def call(self, name: str, arguments: str) -> str: + """Record and execute a tool call.""" + self.calls.append((name, arguments)) + return "tool-result" + + def _build_service() -> tuple[FakeLLMClient, FakeMessageService, LLMService]: """Build an LLM service wired with fakes.""" client = FakeLLMClient() @@ -419,3 +466,82 @@ async def test_stream_response_propagates_session_not_found() -> None: assert client.stream_input is None assert messages.created_assistant_data == [] + + +@pytest.mark.asyncio +async def test_generate_response_executes_tool_calls_then_finishes() -> None: + """Test that requested tool calls are executed and the model is re-invoked.""" + client, messages, service = _build_service() + registry = FakeToolRegistry() + client.responses = [ + LLMResponse( + text="", + tool_calls=[ + LLMToolCall(name="search_notes", arguments="{}", call_id="call-1") + ], + output_items=[{"type": "function_call", "call_id": "call-1"}], + raw=None, + ), + LLMResponse(text="Final answer", raw=_raw_metadata()), + ] + + with patch( + "ai_notes_api.services.llm_service.build_registry", + return_value=registry, + ): + result = await service.generate_response( + user_id=TEST_USER_ID, + message=_user_message(), + ) + + assert registry.calls == [("search_notes", "{}")] + assert client.create_call_count == 2 + assert result.answer == "Final answer" + assert messages.created_assistant_data[0].content == "Final answer" + + +@pytest.mark.asyncio +async def test_stream_response_executes_tool_calls_then_finishes() -> None: + """Test that streaming executes tool calls and re-streams until completion.""" + client, messages, service = _build_service() + registry = FakeToolRegistry() + client.event_batches = [ + [ + LLMStreamEvent( + type="final", + response=LLMResponse( + text="", + tool_calls=[ + LLMToolCall(name="search_notes", arguments="{}", call_id="c1") + ], + output_items=[{"type": "function_call", "call_id": "c1"}], + raw=None, + ), + ) + ], + [ + LLMStreamEvent(type="delta", delta="Done"), + LLMStreamEvent( + type="final", + response=LLMResponse(text="Done", raw=_raw_metadata()), + ), + ], + ] + + with patch( + "ai_notes_api.services.llm_service.build_registry", + return_value=registry, + ): + events = [ + event + async for event in service.stream_response( + user_id=TEST_USER_ID, + message=_user_message(), + ) + ] + + assert registry.calls == [("search_notes", "{}")] + assert client.stream_call_count == 2 + assert [event.type for event in events] == ["final", "delta", "final"] + assert len(messages.created_assistant_data) == 1 + assert messages.created_assistant_data[0].content == "Done" From 0838abe813ac87d0510d60660978b1ecf16a9695 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:03:39 +0300 Subject: [PATCH 71/81] build(uv): add markitdown extras and lower coverage threshold --- pyproject.toml | 4 +- uv.lock | 651 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 651 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0438ed0..61d02d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "pgvector>=0.4.2", "aioboto3>=15.5.0", "tiktoken>=0.13.0", - "markitdown>=0.1.6", + "markitdown[all]>=0.1.6", ] [dependency-groups] @@ -127,7 +127,7 @@ parallel = true [tool.coverage.report] show_missing = true skip_covered = true -fail_under = 90 +fail_under = 85 [tool.pytest.ini_options] filterwarnings = [ diff --git a/uv.lock b/uv.lock index bb65d37..a06b4cc 100644 --- a/uv.lock +++ b/uv.lock @@ -23,7 +23,7 @@ dependencies = [ { name = "fastapi" }, { name = "greenlet" }, { name = "loguru" }, - { name = "markitdown" }, + { name = "markitdown", extra = ["all"] }, { name = "openai" }, { name = "passlib" }, { name = "pgvector" }, @@ -66,7 +66,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.136.3" }, { name = "greenlet", specifier = ">=3.5.1" }, { name = "loguru", specifier = ">=0.7.3" }, - { name = "markitdown", specifier = ">=0.1.6" }, + { name = "markitdown", extras = ["all"], specifier = ">=0.1.6" }, { name = "openai", specifier = ">=2.41.1" }, { name = "passlib", specifier = ">=1.7.4" }, { name = "pgvector", specifier = ">=0.4.2" }, @@ -410,6 +410,119 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "azure-ai-contentunderstanding" +version = "1.2.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/6c/f9af836a30b5299b10304d6b5ec645f8dbb1857429fa0191e41f86825d70/azure_ai_contentunderstanding-1.2.0b2.tar.gz", hash = "sha256:0ccef3c8087759ca788aabcc9af7b22cd8ada2df0236bf63563f4974c2d8cfcd", size = 265922, upload-time = "2026-06-11T02:24:56.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/08/bbe98d886deeeae320bb2d54b6cf132b1d811478232076d6e0d16d1aafaa/azure_ai_contentunderstanding-1.2.0b2-py3-none-any.whl", hash = "sha256:284885c9a45ef50f3938714cb6636c675c1ce9436dc0d561ff425fc124990379", size = 113072, upload-time = "2026-06-11T02:24:58.47Z" }, +] + +[[package]] +name = "azure-ai-documentintelligence" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" }, +] + +[[package]] +name = "azure-core" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, +] + +[[package]] +name = "azure-identity" +version = "1.26.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/cd/0136f0a52b5d8c351b7009478afa63db17cdcaa0d662288100a7c41996e9/azure_identity-1.26.0b2.tar.gz", hash = "sha256:bb218a6ac7aa7b7b4bc115e2b48aa757b426b41a30c3914b69962942e7769af3", size = 293772, upload-time = "2026-02-12T02:14:35.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/42/e5a373564989b150c9d5e9420172492c195b5e26c4989e84f64353ad315c/azure_identity-1.26.0b2-py3-none-any.whl", hash = "sha256:9b08baa7875cea1295442b4a9f0eae68848c39034d771fb218d79759ad68ec02", size = 197287, upload-time = "2026-02-12T02:14:37.293Z" }, +] + [[package]] name = "bcrypt" version = "4.0.1" @@ -535,6 +648,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -650,6 +808,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, ] +[[package]] +name = "cobble" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/7a/a507c709be2c96e1bb6102eb7b7f4026c5e5e223ef7d745a17d239e9d844/cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa", size = 3805, upload-time = "2024-06-01T18:11:09.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/e1/3714a2f371985215c219c2a70953d38e3eed81ef165aed061d21de0e998b/cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44", size = 3984, upload-time = "2024-06-01T18:11:07.911Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -763,6 +930,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + [[package]] name = "cyclonedx-python-lib" version = "11.11.0" @@ -883,6 +1100,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + [[package]] name = "fastapi" version = "0.137.1" @@ -1122,6 +1348,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1284,6 +1519,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] +[[package]] +name = "lxml" +version = "7.0.0a3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/4f/7857f1ad3949c5f4f1bd4a06b0cc80d8b4418294a11b0ba9091bd66808b3/lxml-7.0.0a3.tar.gz", hash = "sha256:032fbe02d001d2ad7ad3bd2d4d387d651adf7f910bdf85d1633ff7ea4fe26011", size = 5079184, upload-time = "2026-06-17T02:00:37.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/20/43167780389fe1b2b6ca4e794a3dd0319cc4d579b4d25130f9f2a6c36483/lxml-7.0.0a3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0175f1aa2a8b2c3f4f075b8284d313f26f0ec7131a4fa885ca1cfc2f01d292fc", size = 8756469, upload-time = "2026-06-17T01:57:14.175Z" }, + { url = "https://files.pythonhosted.org/packages/39/db/5406ae111e710fc712a893875bfe804fa13116d0ce44d1b28e9f7786792e/lxml-7.0.0a3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0abee637237fdf33d3374b11b24ffc127a051e54291bbba179a1e00d3068470f", size = 4729965, upload-time = "2026-06-17T01:57:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/316033cd2a31e03b914b9627044d19a2e11a4d44b6ae4ec43a088a19a115/lxml-7.0.0a3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32366f4f0214c6b3d4115150d282b93d6d27dfd56195656839346ecb76a661d6", size = 5022373, upload-time = "2026-06-17T01:57:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f1/aa35ecc68d06a65bcb276e65c39e2bb737495ac43156d9142574d1a5f317/lxml-7.0.0a3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68013de481cc2b1e4865b12dc9ed6b5ac729b2b5e724627ec96dfb2717d1f92a", size = 5196678, upload-time = "2026-06-17T01:57:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/11f36f691ed18b5868c98b0ea77e72fdbb30ff3e50bd6e49431188da0ea4/lxml-7.0.0a3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9733b1aa51445caa0b7e0b1ec3c96992d5a258c5a2e0503cbfaf209e51b8630d", size = 5087754, upload-time = "2026-06-17T01:57:24.88Z" }, + { url = "https://files.pythonhosted.org/packages/1b/38/5b120e56da956690434a8ad92701cc044f5c472f80a28f960820d4efd8a9/lxml-7.0.0a3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b55e976e5d0cdbc69a7fbb90c3d30611624e506daae2153cced20829a9a420e", size = 5683904, upload-time = "2026-06-17T01:57:27.172Z" }, + { url = "https://files.pythonhosted.org/packages/12/61/d04e665c19d2155964011f8360961166ba61bbcd185616803690a0ed2b6b/lxml-7.0.0a3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c65416e4ae595ffa11f1ee626abce4620b881b37f2bd385099075884ce9bf949", size = 5330254, upload-time = "2026-06-17T01:57:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/fadfbd4764ef00fe8ccf4201ffa9dbaa5c5790d88454188378e61bb6642d/lxml-7.0.0a3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:63e2fa2c40c127cb42c377f055f04862ef989dae89f236bd49d307f39a23c56c", size = 5458968, upload-time = "2026-06-17T01:57:31.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/0f2342285d89eee1cae9c4ca2f2c4fa4c6f81da2568a3553603794e14307/lxml-7.0.0a3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:ee59784d147d8a8511194f1ce3e529ffee837381eff9e0a7774376bfd735deb5", size = 4727652, upload-time = "2026-06-17T01:57:34.101Z" }, + { url = "https://files.pythonhosted.org/packages/e9/df/6a26ff27cab0d5a14bb8008e5d549f18ae15bdd8c6a5c9ac20f6d04b4e5a/lxml-7.0.0a3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb31ac99ee880bfc9cc080ab887ccd39cd8f4c654443be66f8383fda47c4a120", size = 5266011, upload-time = "2026-06-17T01:57:36.687Z" }, + { url = "https://files.pythonhosted.org/packages/99/65/418feed844ac0557c29b641796f8ec1c744a7aeb472e62a3432cd69a7fae/lxml-7.0.0a3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46950e90aea5be0e4c97e0673ea88bb91cc498fa5e57a99f1be0870fb059ba85", size = 5138070, upload-time = "2026-06-17T01:57:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/52436613f9cfab377128551013d355170473dd4560039eeb2557f1b88166/lxml-7.0.0a3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:220b3006aeadb7c3b97de20bbb0ac1b2a8f2edb59cf9f7e5c214c5f4ac52bf37", size = 4798352, upload-time = "2026-06-17T01:57:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/53/c3/35757eaf9436310c26cbec586a2014c1ff36fb69b5145a9f073cd8391fa1/lxml-7.0.0a3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d7a02ac83e4692b8f193606f34be975b2d305fddb18379fb31a089adab0ccb73", size = 5714097, upload-time = "2026-06-17T01:57:43.857Z" }, + { url = "https://files.pythonhosted.org/packages/f1/14/7595c451b9cbd87e17f2a776aed7357ac08d6859e2f65086d8e5938e89f6/lxml-7.0.0a3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5cc69ba7f37d601fcdc4e1156405e6e3113443906a2faafb261e327db8654c10", size = 5267859, upload-time = "2026-06-17T01:57:46.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/7115c13a6f5c97eaa4f666cdb1e4292fdcba21211140795f9455f4b35f12/lxml-7.0.0a3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:104d9ab97b0838610af24f346a758a48d8d253d434729e45f08bf9251beb58e8", size = 5351182, upload-time = "2026-06-17T01:57:49.417Z" }, + { url = "https://files.pythonhosted.org/packages/f4/35/2948a845ba2b5863f9fce5431effbc0ce8301ed95c56142f1cd692304b30/lxml-7.0.0a3-cp313-cp313-win32.whl", hash = "sha256:30966db870cb2fab2c7dd13c451b46d648b98c89e60efed06b6cffbafdc5018c", size = 3677230, upload-time = "2026-06-17T01:57:51.59Z" }, + { url = "https://files.pythonhosted.org/packages/0e/dc/2fdd48c97293d11262e481eb98c2c02ccb6e6edf72255e4110d28ad3624a/lxml-7.0.0a3-cp313-cp313-win_amd64.whl", hash = "sha256:b1e78404d4c1842a6c2ac57fb37c0132846996b224c3264d0b807f337def1c84", size = 4098666, upload-time = "2026-06-17T01:57:53.863Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d7/66bb36a81a4e2f4301b0726ba635641af80d1207a699a30d15dcb82e735c/lxml-7.0.0a3-cp313-cp313-win_arm64.whl", hash = "sha256:9e85d628005e660a678d84090138caa3ebf4c77c91e33ecda3db97c184f38c07", size = 3736580, upload-time = "2026-06-17T01:57:56.026Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/6878afb67824053206fb0761693070542de400f9b787f902739660c68517/lxml-7.0.0a3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f92a85592443d6ded908d1d9197f8b82a859bb99e5027f01921d085b0b7210fe", size = 8773441, upload-time = "2026-06-17T01:57:59.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/12/e5e4d0a3f22022869be5bdbe8ee6c9253606966e048d2aa83944d7072481/lxml-7.0.0a3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fa4d6b74c518c7a3eb618049c78cfbee3ffa50f99f19734024e2e6789a1a7a99", size = 4738036, upload-time = "2026-06-17T01:58:02.042Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/07c9c98cdb598afb0130498e85d09cef84abaec3d483c51316c43faf1b4f/lxml-7.0.0a3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:daf7cfeb7b50d34d894e7c5ba364952166d026a901ab92811f30318c47d30fd4", size = 5055345, upload-time = "2026-06-17T01:58:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/e9/57/8c5193396ad892942286ec7be7427573f850ca1bb72d449a6340eae4cbb4/lxml-7.0.0a3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e030908fea76f4eead2e854a360d7c63661960d9a28f1d697b4b080c40d0b209", size = 5199441, upload-time = "2026-06-17T01:58:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d0/56fa311bb6e1f0378939b829eac566c4559529322ebbd589f39c5e638791/lxml-7.0.0a3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8f1cc432df57fa6001508385a9edae5f6efc7652cd15ebadc6de11bf96d1b5c", size = 5133690, upload-time = "2026-06-17T01:58:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/8d/29/8cbc247add3db4180600d0d1fdbe2e497f98b7ac92e1f7d41bdfedf0155b/lxml-7.0.0a3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00399952909ee229569a675e63aca514cc0801568c6ecd0e59350e0d1ef224a4", size = 5721946, upload-time = "2026-06-17T01:58:11.749Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/a74fb6137dd18166c7b48a0c1819164be469200bb3a5e2e1adae1300644f/lxml-7.0.0a3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61a7d5a4385faa2bad2ee74fb3a65db5285f49d137ffd28d9714f39bfe9a25d5", size = 5344075, upload-time = "2026-06-17T01:58:14.754Z" }, + { url = "https://files.pythonhosted.org/packages/26/e3/a015d8c2fe03b33c62604769915219ce67a606512f7e8e2c245584781d8a/lxml-7.0.0a3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:06597e008f702cddbf710826c1f04d6257eb5814616ad418613306c452ed5de4", size = 5467057, upload-time = "2026-06-17T01:58:17.456Z" }, + { url = "https://files.pythonhosted.org/packages/60/c2/2577004aa5f7d9ff33f498f7d13b52630df67ab822ce2baaa1c6bd7c115a/lxml-7.0.0a3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:5239700b2be7b629864f7c1da087eaccb2930949be230f06fac0ec884c059112", size = 4702987, upload-time = "2026-06-17T01:58:20.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/62/932de89a67bcd572236a22fa77dae3e5fe0616c41e697aca2343064979bf/lxml-7.0.0a3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39e4fdc563269eaf05dce07ca0371923ac83bc2e73e8ff6a800f7979a74abdbe", size = 5283116, upload-time = "2026-06-17T01:58:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/13/6c/5c13cbd1a305b6fba67687a4ea46b95ff9904e70671d219db52dd882ff38/lxml-7.0.0a3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:afd8504f2ed77b95f67b5c07daaaaaa7c5e8848191307a0845e0eb63a3d217ce", size = 5178869, upload-time = "2026-06-17T01:58:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/f1867e84cb1bcc219378ad767729c649d12dce8936c114c6e327cbd235e9/lxml-7.0.0a3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1bd3c6b209567d33513451704e7dea6d4af3d141113e10af793191da2b449bff", size = 4758587, upload-time = "2026-06-17T01:58:28.477Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/57633607d6e377fb5e9c64c1eeba17e49ed3f55febc2a37ade4e641f66ca/lxml-7.0.0a3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4fc6c4f9966fac48471074a40f255b5dc6863c2408603c3b842d739521ad4", size = 5750272, upload-time = "2026-06-17T01:58:31.08Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ae/2c403d2f9a8ceebb3d479fc5247d6269192ef98b3b89d4f1e45579bf1729/lxml-7.0.0a3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:bb84e340db120c12c11f7979b70c9510812f19e2b966573de82112b2c5d867a0", size = 5281289, upload-time = "2026-06-17T01:58:33.672Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/9ca273f92f8f9016c5dfb144dee9f1b45c83863fd156a760b85fef1b64f1/lxml-7.0.0a3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:56651cd2cf15562454fe574363d0c741579d940c4fde8ab83be7b92ad5099a30", size = 5360963, upload-time = "2026-06-17T01:58:36.331Z" }, + { url = "https://files.pythonhosted.org/packages/26/54/b093e1a9460ba9eb22f214333450986bc39835e37d9f898e5fa5a19286b8/lxml-7.0.0a3-cp314-cp314-win32.whl", hash = "sha256:2cb7c32bf065487ea674d221f31d070de3729c958d8c6a1b793678a9aa8dce63", size = 3734387, upload-time = "2026-06-17T01:59:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2b/c11f8b42717c584f5e118b2942994c4bb5f6d799b3723c0eb7d4c475c5e5/lxml-7.0.0a3-cp314-cp314-win_amd64.whl", hash = "sha256:6b90de57d75b05ca5ae5e46e1d2fe7301758ecc4868c843ea10cfcf83e5da21d", size = 4180740, upload-time = "2026-06-17T01:59:29.609Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c0/9a6639cd8543a7bb6edd1dc649ffe01aa691ef562046a0da64219ce2379c/lxml-7.0.0a3-cp314-cp314-win_arm64.whl", hash = "sha256:1270ad92dac96883358b535cd7be091c8fc71b54e024828e0cf8c4603169ed48", size = 3829176, upload-time = "2026-06-17T01:59:32.931Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/eef3e30e294d3a47b6c3243c95ab26a48dfaac6d3aa8a8c065ec73a19274/lxml-7.0.0a3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:822e9ed689ab0cd44b49fcdb08bf828032e7bb7ba719df48a1d84078fd5c3eee", size = 9038637, upload-time = "2026-06-17T01:58:39.446Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1a/77c4a853361f1d4ccaa562d0c7525694d65e0f787a8d20aa117c2fae8623/lxml-7.0.0a3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:324867f3194a21e73e2f2da9a18360a92caf73d8e3f0f9bbdb275fa35c08de39", size = 4850870, upload-time = "2026-06-17T01:58:42.394Z" }, + { url = "https://files.pythonhosted.org/packages/a0/4b/b161bcece881161735d1ab2cfa7c46e161f1f19352a1d688be08f8af0b90/lxml-7.0.0a3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2d3895582b266d1c2d83325cd1ed969fc04fe8cb4a0738bc50a6bea2ec2c354", size = 5079183, upload-time = "2026-06-17T01:58:45.16Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/16f9b44a85925bc7b642201a32544c96e3717842d357761bcd90aa906739/lxml-7.0.0a3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:375c28e1062917a27c1ed3f384690dd4968683ff4414121111291530a4ee13f0", size = 5222582, upload-time = "2026-06-17T01:58:47.757Z" }, + { url = "https://files.pythonhosted.org/packages/15/f2/75c20c0973a7144620dafca0202719470fba6433179dce4d595a22af0fe6/lxml-7.0.0a3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d5bb9a063a16b9046d723af9582ae27849b49be426174631e9a0284a38dc9e2", size = 5128222, upload-time = "2026-06-17T01:58:50.277Z" }, + { url = "https://files.pythonhosted.org/packages/77/0e/aa4a30adf39c06e00a014f68574a32ed52fe648aa982f01b4ba2c765f9a6/lxml-7.0.0a3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aa9a64a350df2d2ee12bb83092889cc1349836c6b119224beea8d381ec7cb1ca", size = 5612628, upload-time = "2026-06-17T01:58:52.682Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/623230fb3da8f92b522c63cc8f57e0c7f473702ce7f4168ff035f766b2be/lxml-7.0.0a3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ec063673fa1685b5883cfcb55c8d920ee15768c8850dbffea1a94af0b5e57af", size = 5320539, upload-time = "2026-06-17T01:58:55.796Z" }, + { url = "https://files.pythonhosted.org/packages/77/2e/b214f78af91bbdd42f9c61397b65cea40945087b64d29d6d4439b6d4ea94/lxml-7.0.0a3-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c7fc9f5a28376b37e17a7f1306d7bf4117550a5d67f3287b5fe754bf54079379", size = 5421594, upload-time = "2026-06-17T01:58:58.404Z" }, + { url = "https://files.pythonhosted.org/packages/d0/50/9ceb43d561c00404c8ed9ba7946b8ac471c6cd08e1b7b1b345eb2f31dc1e/lxml-7.0.0a3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d90e41f4a924474ec9428075208dc75c00360432ec9a103922ee9c7bb6ec2b4b", size = 4623399, upload-time = "2026-06-17T01:59:00.955Z" }, + { url = "https://files.pythonhosted.org/packages/33/3d/ee3fcec174b2fc4b0a28ed4934ec7046a08d1eab62ebce44d41b6c42084e/lxml-7.0.0a3-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7d4d4266f048bd006d8d15406706b90820928f37b006ee0c34706a66a0686cc", size = 5213260, upload-time = "2026-06-17T01:59:03.531Z" }, + { url = "https://files.pythonhosted.org/packages/fe/74/e5ec8af12f5d4841aee7777ef80763eb2709f7b5545427d37bee4d341a08/lxml-7.0.0a3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1a75d1f11944cbb93b770ea3e93a93eda0e6486df8ae2e943b760d9d505cae", size = 5177225, upload-time = "2026-06-17T01:59:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/876d15b5399a95f0c5d7b55166e333bd006a0f2b66604ad721bd61ddf26e/lxml-7.0.0a3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:42dc13994f4bea2118d62a621911a236269132283b3d3ab6d516097c65e4ca6f", size = 4748192, upload-time = "2026-06-17T01:59:08.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ba/1a83fa87fa4bc58f1cdf5dc02003faf9f207b53e8b19bf185fd11b7c40bd/lxml-7.0.0a3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e5613ce002295d3d744c501e86c181952addb09cd3f799ad3a81a20cbc391405", size = 5650672, upload-time = "2026-06-17T01:59:11.266Z" }, + { url = "https://files.pythonhosted.org/packages/87/86/d036b41d284b2647d4917a2446b06cc784ad0636912d2649b5c1504f82ef/lxml-7.0.0a3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:664f56f94c5e9685e9b3fb6fe60f92c7e49dca8f0a3f95148f402f4b648bb28a", size = 5212150, upload-time = "2026-06-17T01:59:13.87Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dc/dd028a179a5a781fe229df8e2636ff1fccc14feb57ee29ecabc9c245b67e/lxml-7.0.0a3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab36e21ebcaf14c7cff0bd47f6a0f4058ede27ca831a3f00160d5b288a727e05", size = 5344711, upload-time = "2026-06-17T01:59:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bf/7919d64aec9c716c9650c7e548593e8dee9fdb0f9ad39889a5cc05084b77/lxml-7.0.0a3-cp314-cp314t-win32.whl", hash = "sha256:10ab55cd9379a6e256410f5541d588a00590aa3aa6ee196417bc735fa60b9867", size = 4009964, upload-time = "2026-06-17T01:59:19.582Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4d/331d3180f843b8c069fc76ddc71336d97c24c148131a3c0636f5642a356a/lxml-7.0.0a3-cp314-cp314t-win_amd64.whl", hash = "sha256:61eb2097b3b3f889640551bd609140c1d0b7cb04562eddbf08613f0ec580ea80", size = 4539466, upload-time = "2026-06-17T01:59:21.908Z" }, + { url = "https://files.pythonhosted.org/packages/05/f0/abf5e19755a6dbb63ed3a4bddff2fee841f8f756a947ab9a2fa8dc8bb105/lxml-7.0.0a3-cp314-cp314t-win_arm64.whl", hash = "sha256:6e6be955c7416bb1a9da87d0681c5ed3d18653d4e97f406b2c99bdbea42a9b5a", size = 3906882, upload-time = "2026-06-17T01:59:24.473Z" }, +] + [[package]] name = "magika" version = "0.6.3" @@ -1314,6 +1611,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] +[[package]] +name = "mammoth" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cobble" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/3c/a58418d2af00f2da60d4a51e18cd0311307b72d48d2fffec36a97b4a5e44/mammoth-1.11.0.tar.gz", hash = "sha256:a0f59e442f34d5b6447f4b0999306cbf3e67aaabfa8cb516f878fb1456744637", size = 53142, upload-time = "2025-09-19T10:35:20.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/54/2e39566a131b13f6d8d193f974cb6a34e81bb7cc2fa6f7e03de067b36588/mammoth-1.11.0-py2.py3-none-any.whl", hash = "sha256:c077ab0d450bd7c0c6ecd529a23bf7e0fa8190c929e28998308ff4eada3f063b", size = 54752, upload-time = "2025-09-19T10:35:18.699Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -1356,6 +1665,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/30/8031f183ee86ea8ac4e7ea1296bab4cca1bee2fd036a26df69764eb7ca74/markitdown-0.1.6-py3-none-any.whl", hash = "sha256:07b2d5bf87b5c53e13a9f2fdc440df8ccc85e26f40c1e557781727b700049775", size = 70032, upload-time = "2026-05-26T22:44:03.209Z" }, ] +[package.optional-dependencies] +all = [ + { name = "azure-ai-contentunderstanding" }, + { name = "azure-ai-documentintelligence" }, + { name = "azure-identity" }, + { name = "lxml" }, + { name = "mammoth" }, + { name = "olefile" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "pdfminer-six" }, + { name = "pdfplumber" }, + { name = "pydub" }, + { name = "python-pptx" }, + { name = "speechrecognition" }, + { name = "xlrd" }, + { name = "youtube-transcript-api" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1426,6 +1754,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "msal" +version = "1.38.0rc1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/ba/afc0474f72674e1a19155afe7b6b3a8b12359d2aee458e216f4dd8030f5b/msal-1.38.0rc1.tar.gz", hash = "sha256:c0160b98217f84705339189d0fa5099cdec0ffa5986e3d2053f450007a2f1a89", size = 182430, upload-time = "2026-06-05T15:20:47.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/9f/873565789342901574aa85d228c6ed6761addf9f5a4829217e7f94671700/msal-1.38.0rc1-py3-none-any.whl", hash = "sha256:4c3528a473d856f725c8f9b302c364c576e21b5cf43a581273c2682d973c4da3", size = 123766, upload-time = "2026-06-05T15:20:48.981Z" }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, +] + [[package]] name = "msgpack" version = "1.2.0" @@ -1643,6 +1997,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" }, ] +[[package]] +name = "olefile" +version = "0.47" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, +] + [[package]] name = "onnxruntime" version = "1.20.1" @@ -1683,6 +2046,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/d2/ba767f4bbb30776c03d40906a2d3afad716a165ffa1771fc23b8992f7920/openai-2.43.0-py3-none-any.whl", hash = "sha256:65a670b54fadf2268c9e1330133373c963eb779ee969e5cbad419ec2c21dce97", size = 1355077, upload-time = "2026-06-17T17:06:53.614Z" }, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + [[package]] name = "packageurl-python" version = "0.17.6" @@ -1701,6 +2076,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +] + [[package]] name = "passlib" version = "1.7.4" @@ -1719,6 +2138,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pdfminer-six" +version = "20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" }, +] + +[[package]] +name = "pdfplumber" +version = "0.11.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pdfminer-six" }, + { name = "pillow" }, + { name = "pypdfium2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/56/6f450312ba05a27d7713b73857c1a25100dbda04fbc1331b13fb227a607d/pdfplumber-0.11.10.tar.gz", hash = "sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57", size = 102892, upload-time = "2026-06-15T03:31:31.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/9a/07d658e1e7fad860f1c541ab941348125dbdab773be3a0afaf32361866c7/pdfplumber-0.11.10-py3-none-any.whl", hash = "sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580", size = 60047, upload-time = "2026-06-15T03:31:29.702Z" }, +] + [[package]] name = "pgvector" version = "0.4.2" @@ -1731,6 +2177,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "pip" version = "26.1.2" @@ -1945,6 +2449,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -2035,6 +2548,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -2044,6 +2566,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -2053,6 +2589,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pypdfium2" +version = "5.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/78/d9b45abb97a3686643f7c6472a5f7688f2013a373226121dc76b9debbacf/pypdfium2-5.10.1.tar.gz", hash = "sha256:f257d2011eb43c846b7e9f5a802e28646b29732763e4a35dd6ca76f9be580538", size = 272963, upload-time = "2026-06-15T10:09:16.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/49/0b85fcc0d236582143a25cf275b0b4f5d786f51eb07a89ec9c79d43efe18/pypdfium2-5.10.1-py3-none-android_23_arm64_v8a.whl", hash = "sha256:13abf7a9f5e0ddebc8bbcccea5f13ae5abe8a298ea219e125b0fc24c1d2171b4", size = 3409176, upload-time = "2026-06-15T10:08:36.017Z" }, + { url = "https://files.pythonhosted.org/packages/20/d2/2f522c5b2ad5166edf256bb4dbab97de5d07b2573f8a5630ddfcd1f8d4ee/pypdfium2-5.10.1-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:a0dc52b56631e2f7edcdb22bae3b155aa840bec32c5bd05781e90baaecee88e7", size = 2866175, upload-time = "2026-06-15T10:08:37.955Z" }, + { url = "https://files.pythonhosted.org/packages/a7/be/477548c026c2badfdbf4afc3358b7135121fb5bec2e2effcb67e3a674d0b/pypdfium2-5.10.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:ebb9e63f92d15fc41b359fe7a187233dfae37548800e1fa09cb2fc466ac89951", size = 3621427, upload-time = "2026-06-15T10:08:39.969Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9b/0131c7f711b62c6edd6b200e9eb6340be6de4f6dc5baae625e3394d8d5fb/pypdfium2-5.10.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:d04f2050b6b32bb18624688b600543342a4ab3aacf64bf66521a5af72bbc7de1", size = 3682825, upload-time = "2026-06-15T10:08:42.181Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3d/bddfceb6e67e54d6dd1ab6c0f1feff796a89596e40f6345a3b4cc6a3d408/pypdfium2-5.10.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de53d2710ca9509fc2812340acc57c3f043697609068592d87de654f8cabf44", size = 3682206, upload-time = "2026-06-15T10:08:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/dedeb25c6645fc8a5eda24f54e0b1d083b7334ababf5f518bb939d729cc0/pypdfium2-5.10.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10a26ce04795f8ec079e81c707fcb5737061e8a78025babc3d6e36642e9c903a", size = 3413720, upload-time = "2026-06-15T10:08:45.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/69e1fc8b1216005243c6415183bbf6de1cda3f5a06758b3fa4a26a7385c6/pypdfium2-5.10.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be6d2a8d1bcfd777188e7aa55a25c83f34d54e8350ad8810fb32017787d9b0d9", size = 3812913, upload-time = "2026-06-15T10:08:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/72/7f/132455a58ad736d76815c6cd1307532c3f433299d945b9d2f8cc2387c309/pypdfium2-5.10.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf4d2527f79f31c550490cc74c9f32e19385a944630a3ef4cd4d9b6f961fbf77", size = 4223220, upload-time = "2026-06-15T10:08:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8c/4d5804eca598bbe894e0a9a510807e221c1623e7276ce6b68fa2660dc933/pypdfium2-5.10.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ba127750bc3f4161461538d532d74491cd976f584f1753a6cee9cb821338ec1", size = 3738950, upload-time = "2026-06-15T10:08:50.821Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7f/baac59bf14ff914d97789ee0368c22ae233aa857aa9c0726bcb515dbc4d7/pypdfium2-5.10.1-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80f30517ee089dfbbc6e9de6da365b6e8c0ce8f80c40b7d4025a3b1d3bbd8a70", size = 4029869, upload-time = "2026-06-15T10:08:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d7/a5d58a0bcba31a0e37ed636a76ef3d2d215733f28af61637287d061b0c54/pypdfium2-5.10.1-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ad5f5de15febb788c6eb3853e58aceda9cd8c5187c92472abaeecf9558deb0cf", size = 3990927, upload-time = "2026-06-15T10:08:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/da/1a/98eebd14b36812176297cf765d504ebeebc982f895c8a3a9fbd2717797de/pypdfium2-5.10.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8947fa3cd808da33960bdf8ff9e5247aa94ccd94f0b09bb3402e99498d83bcc9", size = 4989624, upload-time = "2026-06-15T10:08:56.284Z" }, + { url = "https://files.pythonhosted.org/packages/1c/20/f2e124d607b8bb90a9f1ce976afff38c70e215cd7ea86af784cb2e8a19dd/pypdfium2-5.10.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:139a6387a3a2652f288e53164268bb03af4fa221d78484ee18407053a60082a3", size = 4535124, upload-time = "2026-06-15T10:08:58.214Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/469ea87f668a32ff3280ea15e522e3a7858d2c80f1ecc320ece1244624c9/pypdfium2-5.10.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:172ff3e10358d66456e27fb0b8b5098e28ec24e51072eb4b5b86077d550e21bb", size = 5229373, upload-time = "2026-06-15T10:09:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/c3/48/c7ed3001f0c5e28114c98bf918c8121e422a65ba7323e1e12f3f28e2d278/pypdfium2-5.10.1-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:09fda0609dc4749c9865a0315c447d6f2583a580693bcabd69980d3fbb22ad51", size = 5140010, upload-time = "2026-06-15T10:09:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bc/00b731bfc1fdc0f3c7d91108fceb54978ff4f5a92336820e5ed6c12535ff/pypdfium2-5.10.1-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:6b01adcfe9aaf7a635a59bed5687fd4fd7b0da292664f050d4ebd2bfa5c70584", size = 4643310, upload-time = "2026-06-15T10:09:04.794Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e8/3ad242233f657c19092a8c83684c8154b8d02a826535a6a2591d91aa5dde/pypdfium2-5.10.1-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:610e14c37d2090b826dccc0604fc7e7612c0ce591190780ae228abdb9abb971e", size = 5087879, upload-time = "2026-06-15T10:09:06.861Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/9d0747a02ac3021ed3db7ac27c5187d97e78b0253a4bdfbf386666968474/pypdfium2-5.10.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f2803020952afa57e1e148adc19369b835154e1fa251a1ca85008ab6466a710f", size = 5047369, upload-time = "2026-06-15T10:09:08.883Z" }, + { url = "https://files.pythonhosted.org/packages/9e/41/7d5187e9527eae81890a3b13193442112ef788d61ddaa68a6d59ac447bec/pypdfium2-5.10.1-py3-none-win32.whl", hash = "sha256:8702bb4f01ddfc8e7757b41b4c2c8392ac17c9f0234476e1e69672ea7c6d6aa0", size = 3680056, upload-time = "2026-06-15T10:09:10.95Z" }, + { url = "https://files.pythonhosted.org/packages/16/1d/c62bd59dd8345cc4b640f942f465633f6b07b859d01ddb648610a7bf5c7c/pypdfium2-5.10.1-py3-none-win_amd64.whl", hash = "sha256:58da5b51fb7884c7d21a05062ab13edb011d1a08dfd9694f3d5d685df62796b9", size = 3812105, upload-time = "2026-06-15T10:09:12.684Z" }, + { url = "https://files.pythonhosted.org/packages/10/d5/21bac39125df8a93e99c04583486b58a62b5997d6b3541e3ad0f69053392/pypdfium2-5.10.1-py3-none-win_arm64.whl", hash = "sha256:e3301c2f7a66fb8cb57dba857d0c9e90215e178f6602a87c5a306cd98513dab8", size = 3600043, upload-time = "2026-06-15T10:09:14.606Z" }, +] + [[package]] name = "pyreadline3" version = "3.5.6" @@ -2161,6 +2726,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] +[[package]] +name = "python-pptx" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "pillow" }, + { name = "typing-extensions" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -2415,6 +2995,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] +[[package]] +name = "speechrecognition" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts" }, + { name = "standard-aifc" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/91/442c0ec260ad94ec1f6cae74fa065811f0e4416e4dc38f658ee1052498e4/speechrecognition-3.17.0.tar.gz", hash = "sha256:bd7e609c2ebea1680e75fc5dfbd8b44388c316f134c8d44fa2a30c0f50b346be", size = 32859772, upload-time = "2026-06-17T11:16:52.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/e7/13e260a9cb53a40177783a882ebdfa437b2414fa21ca6f1cb8d9043b3fc9/speechrecognition-3.17.0-py3-none-any.whl", hash = "sha256:754f2cd9d7fbeff5e05ad91b906350cb7983fd2ef82002d91e117ac44aa94efa", size = 32855722, upload-time = "2026-06-17T11:16:49.316Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.51" @@ -2462,6 +3056,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, ] +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts" }, + { name = "standard-chunk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + [[package]] name = "starlette" version = "1.3.1" @@ -2776,6 +3392,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] +[[package]] +name = "xlrd" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167, upload-time = "2025-06-14T08:46:39.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload-time = "2025-06-14T08:46:37.766Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] + [[package]] name = "yarl" version = "1.24.2" @@ -2840,3 +3474,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] + +[[package]] +name = "youtube-transcript-api" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/32/f60d87a99c05a53604c58f20f670c7ea6262b55e0bbeb836ffe4550b248b/youtube_transcript_api-1.0.3.tar.gz", hash = "sha256:902baf90e7840a42e1e148335e09fe5575dbff64c81414957aea7038e8a4db46", size = 2153252, upload-time = "2025-03-25T18:14:21.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/44/40c03bb0f8bddfb9d2beff2ed31641f52d96c287ba881d20e0c074784ac2/youtube_transcript_api-1.0.3-py3-none-any.whl", hash = "sha256:d1874e57de65cf14c9d7d09b2b37c814d6287fa0e770d4922c4cd32a5b3f6c47", size = 2169911, upload-time = "2025-03-25T18:14:19.416Z" }, +] From aa9cc2896f3dfc45cf2af2531b53095756d0d9c0 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:03:43 +0300 Subject: [PATCH 72/81] chore(compose): set explicit service passwords --- compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose.yml b/compose.yml index 972d2d8..920b8da 100644 --- a/compose.yml +++ b/compose.yml @@ -5,7 +5,7 @@ services: restart: unless-stopped environment: POSTGRES_USER: shrimp - POSTGRES_PASSWORD: shrimp + POSTGRES_PASSWORD: passwordshrimp POSTGRES_DB: ai_notes ports: - "5432:5432" @@ -38,7 +38,7 @@ services: - "9001:9001" environment: MINIO_ROOT_USER: shrimp - MINIO_ROOT_PASSWORD: shrimp + MINIO_ROOT_PASSWORD: passwordshrimp volumes: - ai_notes_minio_data:/data healthcheck: From 7d3ba0a9e5a0ba4efd4d4c07ca66bcea5f66a3a0 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:07:12 +0300 Subject: [PATCH 73/81] ci(github): add s3 env vars to fix settings load --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e81058b..1f4e3bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,11 @@ jobs: CELERY_BROKER_URL: redis://localhost:6379/0 CELERY_RESULT_BACKEND: redis://localhost:6379/0 + S3_ENDPOINT_URL: http://localhost:9000 + S3_ACCESS_KEY_ID: test + S3_SECRET_ACCESS_KEY: test + S3_BUCKET_NAME: documents + steps: - uses: actions/checkout@v4 From c836231c75f85564c7cb281565ce87c8ba6fafd0 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:18:37 +0300 Subject: [PATCH 74/81] feat(celery): add generation message missing error --- src/ai_notes_api/exceptions/__init__.py | 2 ++ src/ai_notes_api/exceptions/generation_job.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/ai_notes_api/exceptions/__init__.py b/src/ai_notes_api/exceptions/__init__.py index ceca6f6..be982e4 100644 --- a/src/ai_notes_api/exceptions/__init__.py +++ b/src/ai_notes_api/exceptions/__init__.py @@ -22,6 +22,7 @@ ) from ai_notes_api.exceptions.generation_job import ( GenerationInProgressError, + GenerationMessageMissingError, GenerationNotFoundError, ) from ai_notes_api.exceptions.message import MessageNotFoundError @@ -46,6 +47,7 @@ "UserNotFoundError", "register_exception_handlers", "GenerationInProgressError", + "GenerationMessageMissingError", "GenerationNotFoundError", "ChatMemoryNotFoundError", "MemoryInProgressError", diff --git a/src/ai_notes_api/exceptions/generation_job.py b/src/ai_notes_api/exceptions/generation_job.py index 89e04f7..6c6639c 100644 --- a/src/ai_notes_api/exceptions/generation_job.py +++ b/src/ai_notes_api/exceptions/generation_job.py @@ -26,3 +26,14 @@ class GenerationNotFoundError(AppException): def __init__(self) -> None: """Initialize the generation not found exception.""" super().__init__("Generation not found") + + +class GenerationMessageMissingError(AppException): + """Exception raised when a completion does not return a persisted message.""" + + status_code: int = 500 + code: str = "GENERATION_MESSAGE_MISSING" + + def __init__(self) -> None: + """Initialize the generation message missing exception.""" + super().__init__("Completion did not return a persisted message id") From 916e5136669b7d16cde74cdc67691affc46d4c47 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:19:19 +0300 Subject: [PATCH 75/81] refactor(ingestion): add typing in chunking --- src/ai_notes_api/ingestion/chunking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai_notes_api/ingestion/chunking.py b/src/ai_notes_api/ingestion/chunking.py index 6a932e3..91a3e96 100644 --- a/src/ai_notes_api/ingestion/chunking.py +++ b/src/ai_notes_api/ingestion/chunking.py @@ -72,7 +72,7 @@ def _chunk() -> list[TextChunk]: encoding = tiktoken.get_encoding(self.encoding_name) tokens = encoding.encode(text) - chunks = [] + chunks: list[TextChunk] = [] step = self.chunk_size - self.overlap From aeb9001d28d99d07cd4bc37d5953d0c1df82ecbc Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:19:52 +0300 Subject: [PATCH 76/81] refactor(mypy): fix error message slice --- src/ai_notes_api/services/document_processing_job.py | 4 +++- src/ai_notes_api/services/generation_job.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ai_notes_api/services/document_processing_job.py b/src/ai_notes_api/services/document_processing_job.py index 936015a..9302106 100644 --- a/src/ai_notes_api/services/document_processing_job.py +++ b/src/ai_notes_api/services/document_processing_job.py @@ -108,7 +108,9 @@ async def set_job_failed( processing_job = await self.get_by_id(job_id) processing_job.status = DocumentProcessingJobStatus.FAILED - processing_job.error = error_message[: self.ERROR_MAX_LENGTH] + processing_job.error = ( + error_message[: self.ERROR_MAX_LENGTH] if error_message else None + ) processing_job.finished_at = datetime.now(UTC) return await self.processing_jobs.update(processing_job) diff --git a/src/ai_notes_api/services/generation_job.py b/src/ai_notes_api/services/generation_job.py index 360b04b..5432b7b 100644 --- a/src/ai_notes_api/services/generation_job.py +++ b/src/ai_notes_api/services/generation_job.py @@ -216,7 +216,9 @@ async def set_job_failed( generation_job = await self.get_by_id(generation_id) generation_job.status = GenerationJobStatus.FAILED - generation_job.error = error_message[: self.ERROR_MAX_LENGTH] + generation_job.error = ( + error_message[: self.ERROR_MAX_LENGTH] if error_message else None + ) generation_job.finished_at = datetime.now(UTC) return await self.generations.update(generation_job) From fdd9538ecf76cd89004a828941fc43aa8d04d340 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:20:20 +0300 Subject: [PATCH 77/81] feat(celery): add message existing check --- src/ai_notes_api/workers/tasks/generation.py | 26 ++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/ai_notes_api/workers/tasks/generation.py b/src/ai_notes_api/workers/tasks/generation.py index 0277a84..2dc8e58 100644 --- a/src/ai_notes_api/workers/tasks/generation.py +++ b/src/ai_notes_api/workers/tasks/generation.py @@ -9,6 +9,7 @@ from loguru import logger from ai_notes_api.db.session import worker_session +from ai_notes_api.exceptions import GenerationMessageMissingError from ai_notes_api.integrations import openai_client from ai_notes_api.llm import LLMClient from ai_notes_api.repositories import ( @@ -18,6 +19,7 @@ MessageRepository, NoteRepository, ) +from ai_notes_api.schemas.completion import ChatCompletionResponseSchema from ai_notes_api.schemas.message import UserMessageCreateSchema from ai_notes_api.services import ( ChatSessionService, @@ -29,6 +31,25 @@ from ai_notes_api.workers.celery_app import celery_app +def _require_message_id(completion: ChatCompletionResponseSchema) -> UUID: + """Return the persisted message id of a completion or raise if missing. + + Args: + completion (ChatCompletionResponseSchema): Completion produced by the LLM + service. + + Returns: + UUID: Identifier of the persisted assistant message. + + Raises: + GenerationMessageMissingError: If the completion has no persisted message id. + """ + if completion.message_id is None: + raise GenerationMessageMissingError() + + return completion.message_id + + @celery_app.task(name="generation.run") def run_generation_job(job_id: str) -> None: """Run a queued generation job. @@ -67,7 +88,8 @@ async def _run_generation_job(job_id: UUID) -> None: memory_repository=memories_repository, ) generation_service = GenerationJobService( - generation_repository=generation_repository + generation_repository=generation_repository, + session_service=sessions_service, ) generation = await generation_service.get_by_id(job_id) @@ -97,7 +119,7 @@ async def _run_generation_job(job_id: UUID) -> None: await generation_service.set_job_completed( generation_id=generation.id, - message_id=completion.message_id, + message_id=_require_message_id(completion), ) await session.commit() From 8bbc89472a84a981de06ad3793d58c0543fe2705 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:20:59 +0300 Subject: [PATCH 78/81] fix(mypy): import ignore --- src/ai_notes_api/db/models/document_chunk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai_notes_api/db/models/document_chunk.py b/src/ai_notes_api/db/models/document_chunk.py index dd6ce8e..b0822b7 100644 --- a/src/ai_notes_api/db/models/document_chunk.py +++ b/src/ai_notes_api/db/models/document_chunk.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING from uuid import UUID, uuid4 -from pgvector.sqlalchemy import Vector +from pgvector.sqlalchemy import Vector # type: ignore[import-untyped] from sqlalchemy import ForeignKey, String, Text, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship From 36fbc25094ca936c2d01b8e34aef3c0f66278781 Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:25:32 +0300 Subject: [PATCH 79/81] build(uv): allow prerelease & update packages --- pyproject.toml | 2 +- uv.lock | 563 +++++++++++++++++++++++++------------------------ 2 files changed, 292 insertions(+), 273 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61d02d3..105146d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ requires = ["hatchling>=1.26.0"] build-backend = "hatchling.build" [tool.uv] -prerelease = "if-necessary-or-explicit" +prerelease = "allow" [tool.hatch.build.targets.wheel] packages = ["src/ai_notes_api"] diff --git a/uv.lock b/uv.lock index a06b4cc..9114b3e 100644 --- a/uv.lock +++ b/uv.lock @@ -5,11 +5,14 @@ resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform != 'win32'", "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform != 'win32'", "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", "python_full_version < '3.14' and sys_platform != 'win32'", ] +[options] +prerelease-mode = "allow" + [[package]] name = "ai-notes-api" version = "0.4.0" @@ -310,14 +313,14 @@ wheels = [ [[package]] name = "anyio" -version = "4.14.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] [[package]] @@ -557,11 +560,11 @@ wheels = [ [[package]] name = "billiard" -version = "4.2.4" +version = "4.3.0rc1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/1e/7a7ccf241aecdf983cc1708e5b92249739e9fa52b36ec5dc53244a684838/billiard-4.3.0rc1.tar.gz", hash = "sha256:641752da7933a10fb265a24764efcb4cd99008cded1fef9e8da01f8f42918627", size = 158097, upload-time = "2026-02-26T14:22:32.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b4/f0cf8f0bd627d80551137eb5bb6c465b499e30a8fec63921d6237fe8c892/billiard-4.3.0rc1-py3-none-any.whl", hash = "sha256:e161242c6d65e9d26e30deca1cdfdb82bde031d92d61d17dc09fc80132f8c740", size = 87435, upload-time = "2026-02-26T14:22:30.904Z" }, ] [[package]] @@ -761,14 +764,14 @@ wheels = [ [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -840,7 +843,7 @@ wheels = [ [[package]] name = "commitizen" -version = "4.16.3" +version = "4.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -856,78 +859,63 @@ dependencies = [ { name = "termcolor" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/cc/d87b094ef858c67febcd1d8902352c84b42c9ebc8221d6f2e9d553273358/commitizen-4.16.3.tar.gz", hash = "sha256:5cdca4c02715cc770312f4b505c65a6c39024c73ece41b943bccaf81c44436ed", size = 66772, upload-time = "2026-05-30T06:34:21.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/8a/4fccfa29c95536ac6dc98dc09a676cc2bce60d72f68b8e280278c6674669/commitizen-4.16.4.tar.gz", hash = "sha256:bb2fda50da381979e308d4a443d349598de2487d9e40d2e4f55a1f115ca38a8c", size = 66771, upload-time = "2026-06-22T06:32:50.06Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/35/c7995b1e66159193dd31ed5628d59acbaf4611811645eedf0fb2d5a91946/commitizen-4.16.3-py3-none-any.whl", hash = "sha256:ce1be39fe98a16725fd0c960daf0f360acac86db7ae8db1e1df8d3541005b5be", size = 88927, upload-time = "2026-05-30T06:34:20.006Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8f/496168ab853b325f62075e78ceb611d975ffc58116e4fabcca17725ac53c/commitizen-4.16.4-py3-none-any.whl", hash = "sha256:054e957491e3c897226a631e5bbeca5f5db772ce23412a5d588a4d7b9bb0bbc2", size = 88933, upload-time = "2026-06-22T06:32:48.615Z" }, ] [[package]] name = "coverage" -version = "7.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, - { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, - { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, - { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, - { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, - { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, - { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, - { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, - { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, - { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, - { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, - { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, - { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, - { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, - { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, - { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, - { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, - { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, - { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, - { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, - { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, - { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, - { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, - { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, - { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, - { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, - { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, - { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, - { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, - { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, - { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, - { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, - { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, - { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, +version = "7.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398, upload-time = "2026-06-22T23:10:25.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/83/7fefbf5df23ed2b7f489907564a7b34b9b07098128e12e0fdfa92626e456/coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c", size = 220699, upload-time = "2026-06-22T23:08:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a", size = 221068, upload-time = "2026-06-22T23:08:55.571Z" }, + { url = "https://files.pythonhosted.org/packages/20/86/4f5c45d51c5cd10a128933f0fd235393c9146abbfd2ce2dfa68b3267ead3/coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027", size = 252060, upload-time = "2026-06-22T23:08:57.464Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73", size = 254657, upload-time = "2026-06-22T23:08:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/639ceb1bc8038fd0d66768278d5dc22df3391918b8278c2a21aa2602a531/coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9", size = 255892, upload-time = "2026-06-22T23:09:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/8b/96/002094a10e113512500dc1e10430a449417e17b0f90f7d496bcb820208b7/coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de", size = 258026, upload-time = "2026-06-22T23:09:03.017Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/286a5d2fad9c4bee59bd724feeb7d5bf8303c6c9200b51d1dd945a9c72b0/coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd", size = 252285, upload-time = "2026-06-22T23:09:04.773Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/a17753a0b12dd48d0d50f5fab079ad99d3be1eac790494d89f3a417ca0b9/coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5", size = 254023, upload-time = "2026-06-22T23:09:06.513Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/a76c6ceba6a2c313f905310abf2701d534cada22d372db11731831e9e209/coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb", size = 251989, upload-time = "2026-06-22T23:09:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/d9/39/353013a75fec0fb49f7553519f9d52b4441e902e5178c93f38eb6c07cedb/coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f", size = 256144, upload-time = "2026-06-22T23:09:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/29/0e/613878555d734def11c5b20a2701a15cb3781b9e9ea749da27c5f436e928/coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498", size = 251808, upload-time = "2026-06-22T23:09:12.057Z" }, + { url = "https://files.pythonhosted.org/packages/af/76/359c058c9cfdcf1e8b107663881225b03b364a320017eda24a2a66e55102/coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0", size = 253579, upload-time = "2026-06-22T23:09:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d9/4ba2f060933a30ebe363cef9f67a365b0a317e580c0d5d9169d56a73ef1c/coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37", size = 222741, upload-time = "2026-06-22T23:09:15.636Z" }, + { url = "https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994", size = 223283, upload-time = "2026-06-22T23:09:17.478Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/51d2aac6417523a286f10fb25f09eb9518a84df9f1151e93ff6871f34849/coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150", size = 222678, upload-time = "2026-06-22T23:09:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741, upload-time = "2026-06-22T23:09:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068, upload-time = "2026-06-22T23:09:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117, upload-time = "2026-06-22T23:09:25.458Z" }, + { url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622, upload-time = "2026-06-22T23:09:27.523Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968, upload-time = "2026-06-22T23:09:29.43Z" }, + { url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284, upload-time = "2026-06-22T23:09:31.397Z" }, + { url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143, upload-time = "2026-06-22T23:09:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976, upload-time = "2026-06-22T23:09:35.351Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942, upload-time = "2026-06-22T23:09:37.313Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220, upload-time = "2026-06-22T23:09:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756, upload-time = "2026-06-22T23:09:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413, upload-time = "2026-06-22T23:09:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946, upload-time = "2026-06-22T23:09:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436, upload-time = "2026-06-22T23:09:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861, upload-time = "2026-06-22T23:09:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474, upload-time = "2026-06-22T23:09:51.417Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738, upload-time = "2026-06-22T23:09:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101, upload-time = "2026-06-22T23:09:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225, upload-time = "2026-06-22T23:09:57.904Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643, upload-time = "2026-06-22T23:09:59.938Z" }, + { url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762, upload-time = "2026-06-22T23:10:01.943Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208, upload-time = "2026-06-22T23:10:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096, upload-time = "2026-06-22T23:10:05.936Z" }, + { url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699, upload-time = "2026-06-22T23:10:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433, upload-time = "2026-06-22T23:10:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547, upload-time = "2026-06-22T23:10:12.191Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859, upload-time = "2026-06-22T23:10:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250, upload-time = "2026-06-22T23:10:16.758Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082, upload-time = "2026-06-22T23:10:19.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400, upload-time = "2026-06-22T23:10:21.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469, upload-time = "2026-06-22T23:10:23.405Z" }, ] [[package]] @@ -1111,7 +1099,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.137.1" +version = "0.138.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -1120,9 +1108,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/b1/e5b92c59d2c37817e77c1a8c2fc1f79cdcc04c68253e5406b43e3204cba7/fastapi-0.137.1.tar.gz", hash = "sha256:822360704230d9533d8d9475399613525968aa2f0b5bd2a3ccc9f18c88fd541c", size = 408293, upload-time = "2026-06-15T11:28:20.79Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/58/ff455d9fe47c60abadb34b9e05a304b1f05f5ab8000ac01565156b6f5e43/fastapi-0.138.0.tar.gz", hash = "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061", size = 419240, upload-time = "2026-06-20T01:18:05.259Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/380b9a5922f4340e51c309cde09e5bd32e62f02302971bee30dc15aa0624/fastapi-0.137.1-py3-none-any.whl", hash = "sha256:64f6983c59e45c4b9fdc44e57cb8035c2451ee91ea8e8ec042aca37de7cf6b69", size = 121877, upload-time = "2026-06-15T11:28:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" }, ] [[package]] @@ -1782,43 +1770,43 @@ wheels = [ [[package]] name = "msgpack" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/23/6139781ca7aadf656fa8e384fa84693ffb13f299e6931b6526427fe5e297/msgpack-1.2.0.tar.gz", hash = "sha256:8e17af38197bf58e7e819041678f6178f4491493f5b8c8580414f40f7c2c3c41", size = 183017, upload-time = "2026-06-11T04:16:10.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/26/2902c6946ab5c8fe1e46e40842dfc32b8824464ad5cd4725364fd83f7a58/msgpack-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a1d30df1f302f2b7a7404afbac2ab76d510036c34cf34dffb01f704a7288e45", size = 82621, upload-time = "2026-06-11T04:15:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/c9/59/7e6b812629d2f919e586041bffc130e1af32079f71bb20699eed54ed6d92/msgpack-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:581e317112260d8ca488d490cad9290a5682276f309c41c7de237a85ed8799c8", size = 81866, upload-time = "2026-06-11T04:15:25.032Z" }, - { url = "https://files.pythonhosted.org/packages/31/13/8c291196e60aafdbae38f482205d79432297749ac5d412fe638154fb6f1d/msgpack-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6827d12eacc16873eba62408a1b7bbe8ecfb4a8f7ed78a631ae9bae6ad43cf2", size = 405618, upload-time = "2026-06-11T04:15:26.235Z" }, - { url = "https://files.pythonhosted.org/packages/fb/63/68f5d0ea81e167db5f59ddb94dc6f837667062113feff1c73fabf8907061/msgpack-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a186027e4279efa4c8bf06ce30605498d7d0d3af0fba0b9799dce85a3fd4a93c", size = 416468, upload-time = "2026-06-11T04:15:27.732Z" }, - { url = "https://files.pythonhosted.org/packages/73/58/567dddf5c5a2790f673bcd7d80c83466d68e5ee9a9674ebca3db8101c0c8/msgpack-1.2.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a96142c14a11cf1a509e8b9aaf72858a3b742b7613e095ce646913e88ce7bd99", size = 374464, upload-time = "2026-06-11T04:15:29.286Z" }, - { url = "https://files.pythonhosted.org/packages/0d/30/0c2342fc9092e4498045f5f60bca6ccbe4f4d87789778c2300e6fd6efe82/msgpack-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50c220579b68a6085b95408b2eaa486b259520f55d8e363ddc9b5d7ba5a6ac6d", size = 395879, upload-time = "2026-06-11T04:15:30.973Z" }, - { url = "https://files.pythonhosted.org/packages/b9/11/9565b29b58ce3c33e177b490478b7aaeb8f726ecaaeda26d815893c1db5a/msgpack-1.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4dcb9d12ab100ecacdfaaf37a3d72fe8392eacc7054afc1916b12d1b747c8446", size = 371749, upload-time = "2026-06-11T04:15:32.418Z" }, - { url = "https://files.pythonhosted.org/packages/f2/da/7bade19d60b73e2ef73fb76aaf4504c112a70cb760951b7202a0c64b5111/msgpack-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a804727188ab0ebb237fadb303b743f04925a69d8c3247292d1e33e679767c15", size = 410416, upload-time = "2026-06-11T04:15:34.053Z" }, - { url = "https://files.pythonhosted.org/packages/6d/14/c0c619571c02432208a5977a8dbdd3fc65fe1369f8226ca4b6d08cca87d8/msgpack-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1a1ac6ae1fe23298f79380e7b144c8a454e5d05616b0096584f353ba2d750114", size = 64357, upload-time = "2026-06-11T04:15:35.535Z" }, - { url = "https://files.pythonhosted.org/packages/50/a5/de06718460909aa965737fec4cfe8a15dedc6544a8c55feeb6956fa0d6e3/msgpack-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c3c80949d79578f9dc85fd9fb91edfe6694e8a729cd5744634d59d8455fdde3", size = 71057, upload-time = "2026-06-11T04:15:36.83Z" }, - { url = "https://files.pythonhosted.org/packages/c7/52/73446b0141c94a856e22b787c56709c0815fc34f185326577e15b26d8cfe/msgpack-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fcf8f76fa587c2395fd0057c7232dbf071241f9ad280b235adb7ab585289989e", size = 64490, upload-time = "2026-06-11T04:15:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/35/3d/a7e3cdafa8c0cf36c81e2fa848ec4d30cf089459af45b390ad03f9ce6f49/msgpack-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f854fa1a8b55d75d82ef9a905d9cdbeffdf7897c088f6020bd221867da5e56a5", size = 83032, upload-time = "2026-06-11T04:15:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/ca/aa/53ddfba0e347cc4b484e95f629c5850b9e800ca8390c91ffc604407acf87/msgpack-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e90df581f80f53b372d5d9d9349078d729851a3a0d0bd74f53ccb598d01e45b8", size = 82600, upload-time = "2026-06-11T04:15:40.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/fd/e64c2c776e6dbad0af3c963fe0c0dd1ee1ba09efac478b233ab1db41868f/msgpack-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b276ed50d8ac75d1f134a433ae79af8557d0fa25ee5b4737da533dfc2ce382e8", size = 404342, upload-time = "2026-06-11T04:15:41.87Z" }, - { url = "https://files.pythonhosted.org/packages/1b/60/fb9a08e6ccba882dfd370a5837fe3a07572938fdfe954f0f17fdf3e574b9/msgpack-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:544d972459c92aa32e63b800d07c2d9cf2734a3be29cee3a0b478a622850e9f5", size = 412351, upload-time = "2026-06-11T04:15:43.253Z" }, - { url = "https://files.pythonhosted.org/packages/37/4d/df5c575c274fedc68ac9c6c61d045161899efad2afcdc25138efa7edde69/msgpack-1.2.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a070147cc2cf6b8a891734e0f5c8fe8f70ed8739ab30ba140b058005a6e86af4", size = 373331, upload-time = "2026-06-11T04:15:44.754Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a4/c8b98f8191e985ed2003d87664ce3c95cca41db5d0cf6bf4f54327d32ec8/msgpack-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7685e23b0f51745a751629c31713fbefdef8896b31b2bb38299dfa4ae6c0740c", size = 394654, upload-time = "2026-06-11T04:15:46.423Z" }, - { url = "https://files.pythonhosted.org/packages/d4/49/76f036720a602ea24428cfec5ec806f2487c0380b1bff0a2aa3094e15f87/msgpack-1.2.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b9204daeee8d91a7ae5acf2d2a8e3983be9a3025f38aa21bfaefbd7eea84a7dc", size = 370624, upload-time = "2026-06-11T04:15:48.062Z" }, - { url = "https://files.pythonhosted.org/packages/9f/38/40af3d29232833705a43b0fce0d07425cc280a7b92ab2b29932425b40df4/msgpack-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bfc057248609742ebbabf6bcd27fea4fd99c4980584e613c168c9b002318298f", size = 408038, upload-time = "2026-06-11T04:15:49.669Z" }, - { url = "https://files.pythonhosted.org/packages/30/b2/f140ca450524dff4d8d0eb81eb9ed75f8f3e0b1f12e49c5b01617cfa0b1c/msgpack-1.2.0-cp314-cp314-win32.whl", hash = "sha256:a3faa7edf2388337ae849239878e92f0298b4dab4488e4f1834062f9d0c410c9", size = 65823, upload-time = "2026-06-11T04:15:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/4d/13/6517bf966b841c7675ded30701a068ce141f3e698a27aaa35c702d8e078b/msgpack-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:1a3effc392a57744e4681e55d05f97d5ee7b598747d718340a9b4b8a970c40e1", size = 72484, upload-time = "2026-06-11T04:15:52.289Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/1d948420fdaa24de4efdb8012a6a5bebe09c82ee002b8c2ca745e9917f1f/msgpack-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:56a318f7df6bec7b40928d6b0519961f20a510d8baabf6baa393a70444588f0a", size = 66657, upload-time = "2026-06-11T04:15:53.583Z" }, - { url = "https://files.pythonhosted.org/packages/39/16/1674faa1b7bddc19e79b465fd8e88e2cf4e3f7cae90723740701e8541068/msgpack-1.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:afa4a65ab2097795e771a74a3a81ea49534aaeba874eaf426a3332268e045ae6", size = 86093, upload-time = "2026-06-11T04:15:54.98Z" }, - { url = "https://files.pythonhosted.org/packages/dd/24/f241bcfdd9e96b2246289357c5a5e5a496189fd41c5844bee802c116aac7/msgpack-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:409550770632bb28daa70a11d0ed5763f7db38f40b06f7db9f11dd2794d01102", size = 86372, upload-time = "2026-06-11T04:15:56.381Z" }, - { url = "https://files.pythonhosted.org/packages/94/c9/57f8ab98a1b21808c27b6dd6029053e0a796ffbb9b371e460dbe997011a9/msgpack-1.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf47e3cd11ce044965a9736a322afdd390b31ed602d1c1b10211d1a841f1d587", size = 428207, upload-time = "2026-06-11T04:15:57.739Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/4fd4aa739f131ded751ca7167c8ee87d2aab32506ebbeea893b60b51d343/msgpack-1.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:204bc9f5d6e59c1718c0a4a84fc8ff71b5b4562faac257c1a68bca611ecf9b72", size = 426082, upload-time = "2026-06-11T04:15:59.356Z" }, - { url = "https://files.pythonhosted.org/packages/f9/00/db88e9a08fcd6513decaad06cbd5c168142bc3e662fb2f1aca3a563b7aa1/msgpack-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:610154307b27267266368bc1d1c7bb8aeb71da7be9356d403cb2442d9e6399f5", size = 378355, upload-time = "2026-06-11T04:16:00.916Z" }, - { url = "https://files.pythonhosted.org/packages/54/84/eee4dd703d7a600cf46159d621c070b0b9468cf3dbade4ea8272bf5232a4/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6799f157bb63e79f11e2e590cfdb28423fc18dd60c270c3914b5b4586ae36f7e", size = 410848, upload-time = "2026-06-11T04:16:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/0a/195e2c549fd4631eb7f157d016ff15a10c4c1cf82b6d0a9b1edaef5174b1/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:72bd844902cf0a5ac3af2ef742f253cd0b1e5bcd184f49b4fb9a6a1f7bf305e8", size = 376152, upload-time = "2026-06-11T04:16:04.041Z" }, - { url = "https://files.pythonhosted.org/packages/45/9b/bdd143fa79baec411dc658f5686fed680a18b36fcea5fccb6af1b8c7d832/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd450f78d0d81722c80da6cdbf674a856967870a9db2f6c4debc4d8b3c67c", size = 417061, upload-time = "2026-06-11T04:16:05.63Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ce/011ffcd8b919f55196ec53f12ae162e21c879d95afba226894314ff62c07/msgpack-1.2.0-cp314-cp314t-win32.whl", hash = "sha256:378caf74c4c718dfc17590ce68a6d710ed398ff6fcf08237de23b77755730b55", size = 70782, upload-time = "2026-06-11T04:16:07.105Z" }, - { url = "https://files.pythonhosted.org/packages/57/a8/9b8791ca96b1be6b9f659c718271e2cb7f99f73f58aad2dd0b30f750f6c0/msgpack-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:553b42598165c4dd3235994fd6e4b0dfb1ce5f3fd33d94ba9609442643015f38", size = 77899, upload-time = "2026-06-11T04:16:08.353Z" }, - { url = "https://files.pythonhosted.org/packages/5b/04/3fa2dffb87bf598696b86bde7cd642d0a7590520c3fa24cd19611dfebeb7/msgpack-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2825bb1da548d214ab8a810906b7dd69a10f3838b615a2cc46e5172d3cb44f6e", size = 71004, upload-time = "2026-06-11T04:16:09.556Z" }, +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] @@ -2029,7 +2017,7 @@ wheels = [ [[package]] name = "openai" -version = "2.43.0" +version = "2.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2041,9 +2029,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/fa/88d0c58a0c58df7e6758e66b99c5d028d5e0bb49f8812d7203940cd9dbf1/openai-2.43.0.tar.gz", hash = "sha256:e74d238200a26868977002190fb6631613480a93dfe0c9c982e77021ed60a017", size = 785369, upload-time = "2026-06-17T17:06:56.06Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/f5/7c7cb955305cb41f7f3c5fd7e0e38bf6bbf2658468863d4b7b868a5cb8df/openai-2.44.0.tar.gz", hash = "sha256:68a5a5ffad82b8ff7d451c437529fb64f7c3b8123aaf0c021966a882d9e3947d", size = 988753, upload-time = "2026-06-24T20:56:02.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/d2/ba767f4bbb30776c03d40906a2d3afad716a165ffa1771fc23b8992f7920/openai-2.43.0-py3-none-any.whl", hash = "sha256:65a670b54fadf2268c9e1330133373c963eb779ee969e5cbad419ec2c21dce97", size = 1355077, upload-time = "2026-06-17T17:06:53.614Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216, upload-time = "2026-06-24T20:55:58.882Z" }, ] [[package]] @@ -2460,7 +2448,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.14.0a1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2468,9 +2456,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/71/0ae6d4e0a84faf0cd050033416509c44a19dc6dbec5bfdd3e1318cb1feb4/pydantic-2.14.0a1.tar.gz", hash = "sha256:2c3a5627d48f59725564c41b582328a29fa8d9b1108128ef6710463a0136d4fd", size = 844232, upload-time = "2026-05-22T13:23:43.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/91/4e/15d81754895da3f256273432da60fcb7c885177fefaf495c5b4364fadca5/pydantic-2.14.0a1-py3-none-any.whl", hash = "sha256:61a1ea8d65df95b681c1fab9cd7d01b2472837f798df53dc6d0f41f0c217b061", size = 470439, upload-time = "2026-05-22T13:23:40.57Z" }, ] [package.optional-dependencies] @@ -2480,72 +2468,73 @@ email = [ [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a7/74/319859f70c733f341df03823c8ca27ce9003faaac3ffa3110f3af1c8641a/pydantic_core-2.47.0.tar.gz", hash = "sha256:422c1797a7864b2a9a996435aba92fe571fb80190f67a31edbc1ac040c7b51fe", size = 476601, upload-time = "2026-05-22T13:19:00.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/91/810cabda42f7fdd13b349702694e1140f6071fd42d1e542d606d5ad0c2d4/pydantic_core-2.47.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:263560ece98bffbbc0a8047ce60b8a278c859db6a2a4e30d9454b02891045eca", size = 2108548, upload-time = "2026-05-22T13:18:09.878Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/f444a6d63bcbdff3c45291df842941ed56c568f45dd3742f25afb567b5a2/pydantic_core-2.47.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a7fdaff39a66bf66e9037da482575513d2f20bfb02ea9d9222b5cb3b902fc695", size = 1951421, upload-time = "2026-05-22T13:20:32.973Z" }, + { url = "https://files.pythonhosted.org/packages/f5/09/c90dff5407c11e59023161c84df19dd5ed93966a23b2f08cd21d45812ab9/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3e6c8ee5ce8c270bfae09763ae4bbbccfe81090c97d670a621fb86cb1ef6042", size = 1976554, upload-time = "2026-05-22T13:19:04.085Z" }, + { url = "https://files.pythonhosted.org/packages/95/cf/55fbe9f1b396f91005cd1cb7acf2bcf380a1f1b57e261ac78bcdc0ff42f3/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e38cdae682cfec4b3816722dccf6376ca59049726d57dca83c2fe7cc13665589", size = 2055005, upload-time = "2026-05-22T13:18:12.809Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c6/4f7cbaa62582e95c080cdd45fb5d4350ff05dac87ea32930c75dcb427bf1/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc3193ff0b7e2e168f84c6185e70475738c191f3154e0af8f897cd0f8f9a489", size = 2235489, upload-time = "2026-05-22T13:19:13.994Z" }, + { url = "https://files.pythonhosted.org/packages/31/75/16913c82ffd194c537222b3d0e8a05c11681e551add67308ec4af6023724/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57ff41672a615f38af528ee904602be51c653248354e5db8e9252668abe91e68", size = 2308360, upload-time = "2026-05-22T13:18:39.894Z" }, + { url = "https://files.pythonhosted.org/packages/05/9f/b24bb1b764fc360adace5df806a81fd62ef1662df2973891e487c3fd5a2c/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473b9a2b2a1f0dd55cbb32d2b902f93babe7f141a0bb48fb4d3d4d2b3e93e9a0", size = 2092549, upload-time = "2026-05-22T13:17:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/0268ea5972b91178250c0a573bb54c17f697665cd2aa35623087a3589fc6/pydantic_core-2.47.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:195f9c4ac43a7b2a044a7b86631c3352abdb820bed2823ea29f98f779255f459", size = 2123849, upload-time = "2026-05-22T13:18:03.817Z" }, + { url = "https://files.pythonhosted.org/packages/8f/92/26b2147738a89f78925775bb4b34ae53f981ce880ee77ee2c2ccbc49466a/pydantic_core-2.47.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c1cd10d39ef1ff8bcd68b6865bee9c434631ac0608d402fe86e678851c2e2a5", size = 2181733, upload-time = "2026-05-22T13:17:35.308Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/8a858dde4784c7245b7216ab9a71518cb1a898a83ffb5c0509181789de19/pydantic_core-2.47.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7cbe66352fe2b39511d49150e5b52159429cd21f5633a3e801dd2c43829dcdca", size = 2184065, upload-time = "2026-05-22T13:18:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/69/20/ea165877f965622e021f04d63330f9ceb55ede0aefa862863e06a9a610cb/pydantic_core-2.47.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e35192a1d53e55d510d8bb1023c988c7cdae6d94539074971741b2a7656e49a1", size = 2326881, upload-time = "2026-05-22T13:18:41.477Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d6/ad22a6f0941be5a5478e6c378d0ec3c7641be96d7a86a3ea1f9e9830a269/pydantic_core-2.47.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:81de54576de2e20baec76cc5afae2820f9049e6fdc4f357bac3391da02d0ba97", size = 2365574, upload-time = "2026-05-22T13:20:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4e/eb5e3429dd29311e75ab89313cde09b709b0637fd88838d2cb4a98760b02/pydantic_core-2.47.0-cp313-cp313-win32.whl", hash = "sha256:05da6647bdfd3888936ac10aa39b239d659f3c93dff281af0fc5943eb55629dd", size = 1955451, upload-time = "2026-05-22T13:17:31.178Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/ec107c12aa8729c766285d4aa6caa5f5addf8b2ef6cfd35c455a3ca5c66e/pydantic_core-2.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:021220e0a03b66112737ee1fc49759340ce8fafb8d9ade1b7fb366b06033fa45", size = 2071285, upload-time = "2026-05-22T13:19:50.585Z" }, + { url = "https://files.pythonhosted.org/packages/f4/02/b003a55acfeb35823925d1cd502b80e3fe6c5d477865c36815fbefdf49ac/pydantic_core-2.47.0-cp313-cp313-win_arm64.whl", hash = "sha256:55156ee2f6f561ea4e25ab55f84bd70b9c9ed2546a834cb2b038fe10225aaa37", size = 2034804, upload-time = "2026-05-22T13:18:32.64Z" }, + { url = "https://files.pythonhosted.org/packages/ef/46/a4675c783822e229bf04e290ae28cf5a057b00a46039498743b066e0fefd/pydantic_core-2.47.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6e37a6974fbd8fa7cae12285a76970d50b3689ffd6ed7c7fdd176ba81dd22d0e", size = 2104537, upload-time = "2026-05-22T13:18:05.312Z" }, + { url = "https://files.pythonhosted.org/packages/67/bb/f6d6d1dd8362d616b227b55af8bc2d1b7d4ea842facb7c50a82298ba3b9d/pydantic_core-2.47.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2433b8524785cc117e602233bc574879bc8d87f09523edeec51665d5c46cf42d", size = 1951581, upload-time = "2026-05-22T13:18:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/7d01ed8658ccf8beae8a3fcf2cd023c3ba0ab0b19d3f456845a709cece94/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f9f2be064c8bf1189f46f7062fd42765d94f59cfb7db7ef8db19563192110a", size = 1978461, upload-time = "2026-05-22T13:19:15.829Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/dd38ccbd1e68f1c3fd7f8b413a8b32db11bd16d5999c3dc3aed4e54290df/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1f0a9659a2eb161573418e3138f616101ba21bbd2ff04916dca7b6712155e015", size = 2049444, upload-time = "2026-05-22T13:19:06.258Z" }, + { url = "https://files.pythonhosted.org/packages/7d/63/0cb998f3f4ea4255ab6d891e2537d4566cd31630df0d406ce45b00306729/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2995074b99242aa28991e0120a3c881babc139e08750a05b7ea7d140644e091d", size = 2231772, upload-time = "2026-05-22T13:20:05.22Z" }, + { url = "https://files.pythonhosted.org/packages/3f/20/2cacf5c4a1bdda1356351df38f343c6cf13af6194e6e0ee0f7967395793f/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5b224dc04c3ff9b08c24419464eb7f6ad7a1049e12284a00bf80df82bd15fdb", size = 2305322, upload-time = "2026-05-22T13:20:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8f/b7e09086bd48b1ef3f74227ed98907496924a622c7f9315cf661946233c6/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:264361b7236d4374fef6342908f87d084a0d58a2f8d0811e99f714309cb0ba7e", size = 2097748, upload-time = "2026-05-22T13:20:21.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/27/2927049e93f6fbbaa838b2bb656dbd63c6d6fcaab9909d632fd93573ebd9/pydantic_core-2.47.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:53368beaf693f6302a6e33bdefe950857534a04d282811421bd20176d0fb5636", size = 2122971, upload-time = "2026-05-22T13:19:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3b/80b58a0c7f4339f024ad5c5b6316bef895536abf75c45dc85f0c83ae1a92/pydantic_core-2.47.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d2177b44ba7d9d86850f865f362feeaac6a2ed8517a9b505b97ff0b7fdbd7dd", size = 2181287, upload-time = "2026-05-22T13:18:20.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0a/dfcb9575603aaf113d2cae1fb622570046a210276eba2267764b3f83f4f2/pydantic_core-2.47.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fb57a6b538ec7a01b937986bc093aec530fb056135b6bc9cfdd0bf8460c25bc2", size = 2177842, upload-time = "2026-05-22T13:17:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/57/c2/31a198c8180b417d18410e7640c505b39c51ac291aca34f71ac37093f4c4/pydantic_core-2.47.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:18c9c7c3a18e9bdbf1215d913f6bd00e17595dc92949817935cb87a3cf5f1697", size = 2321802, upload-time = "2026-05-22T13:17:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/052d872b61f88d2f71b3d881c08abfbe33cb9d64bd988712bb4fb973001c/pydantic_core-2.47.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2f72a382886ca85bb1247303b9134cf9978c9d454de62e710a1ebcd9d2131927", size = 2363870, upload-time = "2026-05-22T13:18:45.053Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/c8c985d4081dafaa88fe6ac6a41ebe2b82d289c391b2bcbd3508f585e7a9/pydantic_core-2.47.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:cdf4dc2cdd0eacad1bd81c4d25422b4c25b206acae095d2d64e5d5cb7facc6b3", size = 1369335, upload-time = "2026-05-22T13:17:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/42/70/30dfcc18dc5552f7235ae2ffd7b699a1ba0d38ebfcdb97f371c792d61601/pydantic_core-2.47.0-cp314-cp314-win32.whl", hash = "sha256:1e859dd5e06e9807080e14995db131649a77c61131cc464a7fe492a69ce82488", size = 1952107, upload-time = "2026-05-22T13:20:24.07Z" }, + { url = "https://files.pythonhosted.org/packages/fa/49/f5c517ba99b9e1036ead16cedd0fc189dbcbf900fb0b85b746b6a0b9b389/pydantic_core-2.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:234ecade0e358caa1ea516c218b3f61e61e30532cad1a8bb12f2487325838548", size = 2071388, upload-time = "2026-05-22T13:18:46.74Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6c/6b95e00ed7a3c7f78cb8544bc34cb29412c05512af507fc4466912576d50/pydantic_core-2.47.0-cp314-cp314-win_arm64.whl", hash = "sha256:58158d0111e86893bc35aacabe509f951ed303cddf8cdba43533190bde317914", size = 2026046, upload-time = "2026-05-22T13:18:22.112Z" }, + { url = "https://files.pythonhosted.org/packages/22/2c/fa67f96f381ba3c83e8cd75a6eb3c538e123d8d02e19d80383bff01ffda0/pydantic_core-2.47.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:24017fd3befd4d7cc4a8c71f4a1e9a44d29fdc91723c5446b0e795ab808adee7", size = 2102407, upload-time = "2026-05-22T13:17:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/99abe6c33db3dda235a2095eae5fdd03266857abd9be1c50ed97a59587c3/pydantic_core-2.47.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dfa0820888cc4549fcce7e6bd8affa7d75198d885ccd0bb0760def4bb8461862", size = 1931963, upload-time = "2026-05-22T13:18:23.776Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/96d1884f65737f793e64b6b8745ee793e3cb4735e086ead0a40e5349294e/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1d507f331756f7066cde7c9f35ed78fa78223a54369dbc8a34d6da7a5074fa1", size = 1973492, upload-time = "2026-05-22T13:17:22.128Z" }, + { url = "https://files.pythonhosted.org/packages/07/8c/0504f091c75b229cd07d61b45464d7c5291c3ecf2e7d847f215fe81cf5e5/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c4a885fb50c05903bc703a00830616e680304fdcdd90fc9535a52e72debe712", size = 2035548, upload-time = "2026-05-22T13:17:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bf/e9f69c55ef6c945d29e4afb306f62a15354bfd09f1ee25d9aecd4c6cbb82/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b107cbd764bf68f12a57c7aa5846d868bc7463490a1ac2d0f19bffef624c5a9", size = 2238356, upload-time = "2026-05-22T13:19:10.309Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/dfbd214487033093f5946c1fd5915ae5dc6b278efe764397b5e2ef8092a8/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b5f46412c8226d0c8f1f423c324c75afe342e4b854836933579fb484f68598c", size = 2284799, upload-time = "2026-05-22T13:17:27.959Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ec/f80488fa26a6b27bd0d1267f35b0fce3bfeec23ca97021085a94ccd3adc3/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90cbf7e35d597503cbdb5cd85409cbb75f377290bc7e8e37cc5dfe4f5cc66cf8", size = 2107895, upload-time = "2026-05-22T13:18:27.282Z" }, + { url = "https://files.pythonhosted.org/packages/9b/1d/67ac210745f63a982b145d8f281dc76bd347d98052792f087224a178da54/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04ee7ba7172cb4484af51b2890f19069d35773698abc8c6ebb651f52fbf41134", size = 2102797, upload-time = "2026-05-22T13:19:40.139Z" }, + { url = "https://files.pythonhosted.org/packages/6c/75/f3b9bd4d88fcaad9a5abf2ec6969f37a70e34f40a5f8d2d78077f168e83d/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a7acb3e120ddba94372b8146e62ea3a0bce203180e34641c817c87f995c91e0", size = 2160123, upload-time = "2026-05-22T13:19:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/e94ac13bd17c341e86c4b67deb47b63718208fe413034fdb01e7a4bf1dd8/pydantic_core-2.47.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:1a2f7ceb58013d167d8c96f10ec9b3a137018c819ba356f68ff1cb74302fd22e", size = 2164386, upload-time = "2026-05-22T13:18:48.372Z" }, + { url = "https://files.pythonhosted.org/packages/36/e5/862ff195402f5b08db9ad4d9ebe7cbed993f51528aab7edcb62531442556/pydantic_core-2.47.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:482b097637fec5037eb13fe9f9d5fe47e568a5b451d686bc2b854076cc0b50ca", size = 2303767, upload-time = "2026-05-22T13:18:50.211Z" }, + { url = "https://files.pythonhosted.org/packages/c2/05/c8ebbb97cf44686fc9cc64a6bb86ac72a8871426a9f61417e395d77c4894/pydantic_core-2.47.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a8c7f5fad73eb404f4b84c75f3d9d3865b748ded248b7366341db6e516fc502b", size = 2361148, upload-time = "2026-05-22T13:18:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/4695ec6ce81eda7cc822383de993c5b2c289cfc84d3f032a30c55eb238fc/pydantic_core-2.47.0-cp314-cp314t-win32.whl", hash = "sha256:f343c39928097175acf2f7d0cba5c00b0f62265d88a173a4ce264266ac849bd9", size = 1939588, upload-time = "2026-05-22T13:19:42.151Z" }, + { url = "https://files.pythonhosted.org/packages/96/4d/bc2e188ab648b3ab284e80a0340c5d5b9723c22572c6e36ba39eb300e718/pydantic_core-2.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:52d40e074da44e42b2425aedebd513e405a31807036ef597175117a9b01743a6", size = 2049697, upload-time = "2026-05-22T13:18:08.244Z" }, + { url = "https://files.pythonhosted.org/packages/6f/46/ca1e6813e029993e57340c6a37740450c51919e3809472f79060f4d64cf0/pydantic_core-2.47.0-cp314-cp314t-win_arm64.whl", hash = "sha256:25cac08c9735e61e5c0b9f7a85c438661fc0e9da226afdfa984c3da6c5942a5a", size = 2025906, upload-time = "2026-05-22T13:17:50.322Z" }, ] [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] @@ -2629,7 +2618,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.0" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2638,9 +2627,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -2791,11 +2780,11 @@ wheels = [ [[package]] name = "redis" -version = "8.0.0" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/ae/ed461cca5780b5fc8b9fe8ca0ed98d89508645fb9d880c24cc42c087678f/redis-8.0.0.tar.gz", hash = "sha256:a00c5355432051ac14e593b8b197fc76c887ee12d55a0984f69328a1115fdc49", size = 5101591, upload-time = "2026-05-28T12:45:13.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c3/928b290c2c0ca99ab96eea5b4ff8f30be8112b075301a7d3ba214a3c8c12/redis-8.0.1.tar.gz", hash = "sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0", size = 5114170, upload-time = "2026-06-23T14:52:37.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/e3/b519734372d305bd547534a9f32e4ce9f98552af753dce72cf3483a0ff0b/redis-8.0.0-py3-none-any.whl", hash = "sha256:c938c18338585009f0bc310f4c7e4e4b4d37639356c4ac072cedf3af570c8dc7", size = 499870, upload-time = "2026-05-28T12:45:11.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0a/c2345ebf1ebe70840ce3f6c6ee612f8fa749cfbd1b03069c53bf0c62aaad/redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743", size = 502406, upload-time = "2026-06-23T14:52:36.137Z" }, ] [[package]] @@ -2887,14 +2876,14 @@ wheels = [ [[package]] name = "requirements-parser" -version = "0.13.0" +version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/96/fb6dbfebb524d5601d359a47c78fe7ba1eef90fc4096404aa60c9a906fbb/requirements_parser-0.13.0.tar.gz", hash = "sha256:0843119ca2cb2331de4eb31b10d70462e39ace698fd660a915c247d2301a4418", size = 22630, upload-time = "2025-05-21T13:42:05.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/1a/5f3c22d38bf1d87d1f4a961489d9eba35c4370a21395562d94410cdd0e73/requirements_parser-0.13.1.tar.gz", hash = "sha256:78811383b2089b6c5197a1431bc2c12ff950245edca39a23eea3460782038dd3", size = 22783, upload-time = "2026-06-18T07:52:25.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/60/50fbb6ffb35f733654466f1a90d162bcbea358adc3b0871339254fbc37b2/requirements_parser-0.13.0-py3-none-any.whl", hash = "sha256:2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14", size = 14782, upload-time = "2025-05-21T13:42:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f9/15b44d5e4401b0013bbcefe3c09d7bfddcce28cc3d41b1d3077bcedf5b1f/requirements_parser-0.13.1-py3-none-any.whl", hash = "sha256:6e385663eb32589d16e5b22bb6e5251a57908e73803ffff438b53cd6ea2056e0", size = 14926, upload-time = "2026-06-18T07:52:24.171Z" }, ] [[package]] @@ -2924,27 +2913,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" }, - { url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" }, - { url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" }, - { url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" }, - { url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" }, - { url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" }, - { url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" }, - { url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" }, - { url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" }, - { url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, +version = "0.15.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" }, + { url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" }, + { url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" }, + { url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" }, + { url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" }, + { url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" }, + { url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" }, + { url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" }, ] [[package]] @@ -3011,49 +3000,65 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.51" +version = "2.1.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, - { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, - { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, - { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, - { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, - { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, - { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, - { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, - { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, - { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, - { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, - { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c1/15/e21c0f10756f47a0b7758326c60b6bdfe6e2a5bd1787f4474d42de78ba79/sqlalchemy-2.1.0b2.tar.gz", hash = "sha256:5894d52de4fe6927f3db054999c66c919f53de082a0782b8db53977c50163de7", size = 10225731, upload-time = "2026-04-16T20:06:52.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/60/d312dcb0554cacdf57d0ea7b022ac150c469e1817d480c54eeb38de15b9d/sqlalchemy-2.1.0b2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c31c8a2811281baa36192975cd3185b1f32d5d6c90dcf56b2f039046e4513c84", size = 2310366, upload-time = "2026-04-16T20:53:16.187Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e5/346a2ae5eb1b855e05df2ff5a0fcf181d1bff1b07aee26dea70ad9654796/sqlalchemy-2.1.0b2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b33853c0520b6aaadd9fe956d40bc4090a28f6c4bb2501bf517a56ec6efd2856", size = 3994223, upload-time = "2026-04-16T21:06:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/2f/60/f12a1fa7004321defc4a557612b947d40ce5ba08720e1137fecac8fb3a36/sqlalchemy-2.1.0b2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390ca8723bbc763e64a3a7b1e8675f3ef481a11c577b16e223c9a8a83281f18b", size = 4032968, upload-time = "2026-04-16T21:10:08.461Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/cb47d28bba19fa8edfe039a27dd7f885b8fdd1223d23197a88130aa352a3/sqlalchemy-2.1.0b2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44949992dad6ba422ea192e7f7fd54e885e664809b4209503024cb342a021c07", size = 3771005, upload-time = "2026-04-17T00:23:12.913Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6b/7045fcec55d44332df938ebd8bd7699a9fd641771c7d1929e060c5eac3e5/sqlalchemy-2.1.0b2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e66b6b6bba68c47f890d680832d9fea189add25da09f45de31996dede11afb3", size = 3931811, upload-time = "2026-04-16T21:06:41.082Z" }, + { url = "https://files.pythonhosted.org/packages/82/17/d5a6a1edcad5d365b6e6764c91139f0fd600fc37b6d6f240be33d54c1ea6/sqlalchemy-2.1.0b2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c086f118c3777f0c9d98e882e3025368d1a8d85d86f7f1bca1000d10df071752", size = 3769800, upload-time = "2026-04-17T00:23:15.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7c/00323465be53057ee1532cbf24abce5831a9af4b23428d6af39ea3e3241b/sqlalchemy-2.1.0b2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2eb3741504f29cb8a7ba5c4477508ee002044f77faf2252f01a8891858dc0bf4", size = 3998681, upload-time = "2026-04-16T21:10:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/da/a4/b70cdc84b5eac684ff42c705b90044788c1c31ac8f6a6a97e78116da8534/sqlalchemy-2.1.0b2-cp313-cp313-win32.whl", hash = "sha256:8fadd85aea6dda07065590948d6a818d2dc95e91b3adda2fad5b89ae672612f2", size = 2249479, upload-time = "2026-04-16T21:19:06.07Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5c/793b927d9f7f369338ae246c65c9b129a69ad7814a311b7280a97663033d/sqlalchemy-2.1.0b2-cp313-cp313-win_amd64.whl", hash = "sha256:dd32b76e46cac9e32743db3bb305920905920a57a6186a24affd918ff167837b", size = 2290568, upload-time = "2026-04-16T21:19:07.964Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/00691534fa873f6c6750a5d2187c23bd96b732e1556c990b51a2e60d49b5/sqlalchemy-2.1.0b2-cp313-cp313-win_arm64.whl", hash = "sha256:1b0118e07946a5b2524a5544d699f142cec26dbcaed323847e2cde9e9bc21aa8", size = 2244194, upload-time = "2026-04-16T20:55:09.597Z" }, + { url = "https://files.pythonhosted.org/packages/f9/79/e62e36859c86735a585c9d5345715b610b7a212d030c64b77e44e0a7491a/sqlalchemy-2.1.0b2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634f66d3b18082c164a81119e8f807aa37b02918cff9cd4a2d4c11944aba4c2b", size = 4288324, upload-time = "2026-04-16T21:03:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/4f/63/0bdd99c64fb4a1cb45e19c4f9c80889fc440be1dafb3c8431cc5e44387db/sqlalchemy-2.1.0b2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25ae52aea03b5316853ecd69e89af36e9eada4368584fec77865d2dfee204213", size = 4221163, upload-time = "2026-04-16T21:16:41.581Z" }, + { url = "https://files.pythonhosted.org/packages/18/48/afd3e736d187daf6c8ce22382568517e7cc88cb5cfaaebf7eb9923e4a911/sqlalchemy-2.1.0b2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bde9ec83286502e4945a73956140dad6a87e83d09343a681d3496fb1ff8881d", size = 3943215, upload-time = "2026-04-17T01:11:34.217Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/07e842537080972cdc0296a25dd3691055f519e6e05285749d603065f13c/sqlalchemy-2.1.0b2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27b851c95cee2b2d6185121847207a72a92fa805f1556b692d738205ff6ef0c7", size = 4173808, upload-time = "2026-04-16T21:03:17.143Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/af4f2104aab8495e84fc953831fa8b6be6660b7a9f7f2212eb3ee094b043/sqlalchemy-2.1.0b2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4bdc555d0165f1167534f1818d8216bbe22b506b74fe3d93ac54d9b00d00bd8f", size = 3946160, upload-time = "2026-04-17T01:11:38.3Z" }, + { url = "https://files.pythonhosted.org/packages/6a/10/e27d149bdf0891987c10a1dfbbbd8a659bfb330b44d214017fa4c8d0ea05/sqlalchemy-2.1.0b2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:628b2ea58acc0d9ba0c040fd455fba957c4a4f6c15b88c931ee036f9ac4a1620", size = 4169141, upload-time = "2026-04-16T21:16:43.891Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/cebace87bb64aa22cef666eb851d39b7f10bc910006c57a9cff21c6993fb/sqlalchemy-2.1.0b2-cp313-cp313t-win32.whl", hash = "sha256:749eed63b8ab195beeaa96ce46147bac24f884c3fdcdfcd6f28e87a1df4259ed", size = 2289047, upload-time = "2026-04-16T21:05:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/67da6dc61fc885a935464091feaa7494f36618422d2ffbe9d16efc749fd0/sqlalchemy-2.1.0b2-cp313-cp313t-win_amd64.whl", hash = "sha256:203b4687d415c9ab9eae6b97cc7d74d7706e615ca8d331d3c600332d742a87ac", size = 2338047, upload-time = "2026-04-16T21:05:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/2801d4e6ea32437e2ba44e659534d217414964d53430ab4050575a41fe7a/sqlalchemy-2.1.0b2-cp313-cp313t-win_arm64.whl", hash = "sha256:4c0224df223d081d6bdb6c5ae44bfa1cbe9b1d34c336630eff99391e3176ae2b", size = 2268451, upload-time = "2026-04-16T20:59:29.952Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e3/5250fc27ef4a8c6e6e490e06d6dc6b32e227cdfe05b6161c1d85a140d658/sqlalchemy-2.1.0b2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:740c20064e64349efea048e71d8a2c5cad5fe3313e70afe326114d59a294fbaa", size = 2312796, upload-time = "2026-04-16T20:53:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2e/e972e7654c0eb0e4c0beabada8e04b691133b7265f586d7feb6f59918b5c/sqlalchemy-2.1.0b2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0af40685238c2887345bc5c795c688ce5cde6dba7a11c12df1bf1dcef1cce26", size = 3992496, upload-time = "2026-04-16T21:06:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/8fdb4026ee9d796f769f7a9fb8c7b15fd1465c3049f6009cf20e9779ccf3/sqlalchemy-2.1.0b2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aabbb7823155d43aad6128b8d4d83fb08684ee67b7417eb47f8196c201864fae", size = 4012697, upload-time = "2026-04-16T21:10:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/d7/14/c98583e3dc1d3a8520019f0b438e9460d0e56d7300370aab951e5fd52937/sqlalchemy-2.1.0b2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17228ea80258e2c959fc8602058930a5634a59a4c1646045e5644bce11dae3fd", size = 3770654, upload-time = "2026-04-17T00:23:17.316Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/287ef5e0f8fea9a1d6fe2db51519fe06238166d19c366c350dbde516ed81/sqlalchemy-2.1.0b2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cd9e2a89b4493aed9a270710886f831ce13b4f2d5544c6c1e646c3a03e3d4ae", size = 3928014, upload-time = "2026-04-16T21:06:44.68Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fb/dcaf5b7fcdd1baee4ea11f7f7282ea181975c2080b05b187fdb0af1fcabf/sqlalchemy-2.1.0b2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:aebcfc64d89018d2565020a39c46b5b6591b87e66a12580d83d1b3e95cd24a8e", size = 3770324, upload-time = "2026-04-17T00:23:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/211916a0b3e8bad05bca2b95aafb968c44570a0b5bc8b5a08b4a7d0ff381/sqlalchemy-2.1.0b2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d4c75722cb3bc040677af773d611d1a8750b22a8240aed25b626344f11d2ce", size = 3980604, upload-time = "2026-04-16T21:10:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/7eee91eb6847203abc7c9e82c395f3502866bf8b5cc197aa3df48f407461/sqlalchemy-2.1.0b2-cp314-cp314-win32.whl", hash = "sha256:56a9b93c5fb8e461462348711754bfd80d2d5021f06fadc8085011af6e5ee816", size = 2254211, upload-time = "2026-04-16T21:19:09.768Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/16c26fc969438c44052eff9ca24c2ce79974bfb5f1ceb6e5ebf57d625abb/sqlalchemy-2.1.0b2-cp314-cp314-win_amd64.whl", hash = "sha256:5a95b25514c8270e2cc96e964360c66a1f8bda7ec4d7d60fa31abd3916cf9d3a", size = 2296013, upload-time = "2026-04-16T21:19:11.283Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/17cbff7a676aaa7c897f51c210cafffee841b3cede97bda5a814c7f4d5a8/sqlalchemy-2.1.0b2-cp314-cp314-win_arm64.whl", hash = "sha256:a4ce8697424b08bb7afc5cf504de744dc66fd2d06d1cc5da7379d46e41a34c17", size = 2252072, upload-time = "2026-04-16T20:55:10.993Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/5408e5171aefb700aa256561976645ca4cd5eb570f2bb6cce8a2040aa269/sqlalchemy-2.1.0b2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d13d1a0e06ea59066cd22d48daf1fd528f4af9c20a1c34628dbce9c3e45c0a3", size = 4286719, upload-time = "2026-04-16T21:03:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ba/04/1b134b0fc0a0b138eaaf0c30ba177f50e972f208f362de68300f433a1f52/sqlalchemy-2.1.0b2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590c7ae9194187e6939d7a6815a69fce3a8aefdb0c72d3fd9deefd495cb32620", size = 4214859, upload-time = "2026-04-16T21:16:46.343Z" }, + { url = "https://files.pythonhosted.org/packages/09/38/b3863fd757d326ebdf1d5160f3a80defccbf2aa44f45e29fed0197dea660/sqlalchemy-2.1.0b2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a81810f39d625bd8d15494002158abd75ea05531d2bf0724100ba48f2018654f", size = 3943408, upload-time = "2026-04-17T01:11:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ef/6331c61603e17b2da39243f169fc2ca764ecaa7b22742cbe3d4a250dc32c/sqlalchemy-2.1.0b2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:db5b65e8c87249865fe0cd210c84d82ec5cebd04f41a4f34f5f99a4b0be83f3c", size = 4173491, upload-time = "2026-04-16T21:03:21.365Z" }, + { url = "https://files.pythonhosted.org/packages/f9/59/b19bab5f783dfe040d60716ec20389517526bbc43de86b01e97d8fbfb553/sqlalchemy-2.1.0b2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5d1933aeb6c6724608b56e32159a0244363040add7dada77c702e68c6080493c", size = 3946891, upload-time = "2026-04-17T01:11:42.997Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d4/fbbbddc901ed4c700460f988b2d6e091c59a2ec7ad86fecb66b407203a30/sqlalchemy-2.1.0b2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:923956e8a29cf90f7485bc24ef7abf0ddfb9bee13c453f2402952979ee5e518c", size = 4166850, upload-time = "2026-04-16T21:16:48.582Z" }, + { url = "https://files.pythonhosted.org/packages/c8/46/22abf1a6bd6cd9a02694e091580e78e70406159e641c8b40063d7e26ed9c/sqlalchemy-2.1.0b2-cp314-cp314t-win32.whl", hash = "sha256:253a90eb23b77bb38b2c4831989a01caa1da8764dfa969c6ebcc90cfdfa61853", size = 2300161, upload-time = "2026-04-16T21:05:46.881Z" }, + { url = "https://files.pythonhosted.org/packages/18/b2/2b5b30c0ec584ab87d2d1027da8981d47885f3e71c48eac967c430731788/sqlalchemy-2.1.0b2-cp314-cp314t-win_amd64.whl", hash = "sha256:869e65a6a03040a2ad17351e620c44d87c205dd33b206b97a7bdfabd3e12ba4e", size = 2355288, upload-time = "2026-04-16T21:05:48.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/23/adc24c4557684b62973b766459ecdee7c7264ea99d10e4b41e4475d16e58/sqlalchemy-2.1.0b2-cp314-cp314t-win_arm64.whl", hash = "sha256:329b401a98c3f6d7d5e54e22f597b0f7b51ba975885e7d8338d0fa3827a81321", size = 2274031, upload-time = "2026-04-16T20:59:31.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/58/56810f8c4fe3fb14665f495852bee9452583b7b0c61cb43c062e9dfa7a97/sqlalchemy-2.1.0b2-py3-none-any.whl", hash = "sha256:98605269d1b7e1d5d3ef680995e4714fc0c5aaa43615684d316079b6dbed826c", size = 1985929, upload-time = "2026-04-16T20:56:46.059Z" }, ] [[package]] name = "sse-starlette" -version = "3.4.4" +version = "3.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, ] [[package]] @@ -3249,11 +3254,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0rc1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0e/4dd5afda382a7e7bf18592053016fba8a9f9bfc3a8a5765787b9d21f716c/typing_extensions-4.16.0rc1.tar.gz", hash = "sha256:7a37af645610662314adfd9063487f4fcbe60e21ec1e52e1b3707d4f8a376e57", size = 113200, upload-time = "2026-06-24T17:49:04.43Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/3e/dc/2ae4dbb753f6c4418a07f4cbcddeffaf16d63a4e0ac1dde736bf0058c3f3/typing_extensions-4.16.0rc1-py3-none-any.whl", hash = "sha256:a1119bae81849f293d9167389101ba6bbe33f2d6c79ba86aa67327d018e9447c", size = 45566, upload-time = "2026-06-24T17:49:03.063Z" }, ] [[package]] @@ -3355,41 +3360,55 @@ wheels = [ [[package]] name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +version = "1.17.4rc1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/e0/c6c3e66c6ca371728de87b44102b61f3fdacc03c8b0b1e4ac5f30d71c5ce/wrapt-1.17.4rc1.tar.gz", hash = "sha256:19c0363cb46f42cf5536c7b9d9c921cc1ae24e55fe4d45c3a19315e9f2aa8964", size = 55653, upload-time = "2026-03-06T05:27:09.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/c9/bfb0840b9d1a3e9478c9d6bd1b5e2fb82fdca7c046bc10e8c44f9273cd46/wrapt-1.17.4rc1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d1a4c658bea05c1b22ae374f74c25b400535f3dccbf795b121153d5628216f0", size = 39037, upload-time = "2026-03-06T05:28:18.986Z" }, + { url = "https://files.pythonhosted.org/packages/41/82/1e234ad6b64cd705557a0a682dbdce499db082a1932f9c95f200ed0843da/wrapt-1.17.4rc1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491d11b84ac47568ee88777304c42d047d33307ec82162235d7e8261ee983eaa", size = 39295, upload-time = "2026-03-06T05:28:26.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d9/2143f5825ef49046b376a2d9136621a7aa66a9e93ccc82b162d9e79ab678/wrapt-1.17.4rc1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16426870299de6370b93760a50ae5bc813548f4666e6e515dcce3ec7601b9c59", size = 88175, upload-time = "2026-03-06T05:27:25.018Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/8add4d24770a2e960f2bb8cb062a83f880a6aa91664b01d6de1e62917e45/wrapt-1.17.4rc1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a35158b0bf2c2d2033eba3c56832e803d73658dc4e92f14f1ea4c92ab0dfaafe", size = 88320, upload-time = "2026-03-06T05:27:37.675Z" }, + { url = "https://files.pythonhosted.org/packages/9c/34/c47fd4837b07b9f8ae8cfe749ea0d6fe5ea506c2d324850f3067f5f66ca2/wrapt-1.17.4rc1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a33bff65de96bc32f7f1df1492c2808068070ed0f42f1fcef2b47846f6a6a03a", size = 84302, upload-time = "2026-03-06T05:21:09.738Z" }, + { url = "https://files.pythonhosted.org/packages/9d/b0/20542954e5929383f55da30d4b9a47764866a2d253d8bea0a5d366ea1e7c/wrapt-1.17.4rc1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:931dae558932c8ba8e4de77ce92ed505fe5a8fd9dab66a2cbbc9d5d3a3a32bb4", size = 87210, upload-time = "2026-03-06T05:27:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ff/37a5295d7f01b270186191035f67142f3052882210f673b9a62f82fcfc9f/wrapt-1.17.4rc1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ea3a5f62f5980031aaf6e28074cb17cea8df06cb828bcd2882d525f7ccc2f9", size = 83709, upload-time = "2026-03-06T05:27:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/05/ff/11f668fba8ab6436c3a0167d0dc2aacb1c9fca675c1268a83d9106457b0b/wrapt-1.17.4rc1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b302dc5e126057f74b82223c3b19a41dfeead10292667be1538985ef75034f3b", size = 87866, upload-time = "2026-03-06T05:27:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/41/b7e49896146dd95fc8e9ecda84ede824c5d34105dd84aa5f4e108a0c137a/wrapt-1.17.4rc1-cp313-cp313-win32.whl", hash = "sha256:27bf0d37ebcd4a43e8369eaf60dd9ea45f30933a921453f61bd6476ffe39bbfb", size = 36810, upload-time = "2026-03-06T05:28:25.862Z" }, + { url = "https://files.pythonhosted.org/packages/86/8d/ba014ec122b07b6441eb9ed341514045a4c79677186623733be460c379b3/wrapt-1.17.4rc1-cp313-cp313-win_amd64.whl", hash = "sha256:22e85eab852e7182c41acef5f9d95d5d63a1b115910951fb38feccf67b514818", size = 38977, upload-time = "2026-03-06T05:26:55.473Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ef/6561940fba308d086f5967827c63bce7dbf8c54717bc33c7f523f0018400/wrapt-1.17.4rc1-cp313-cp313-win_arm64.whl", hash = "sha256:5be27331b6eae2317350c4adee1cf92edc0866cd7db726f574f10c8db227c134", size = 36944, upload-time = "2026-03-06T05:27:00.79Z" }, + { url = "https://files.pythonhosted.org/packages/58/3e/1ab40e5f926d0650fd5b7e23cebcdd4eab6bed961ac6e7ed5307638ddd27/wrapt-1.17.4rc1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:474a45ee2dfa6bb8c1a2a63fbc91c53da010caece85464a334fdb9aabafb6ecb", size = 40438, upload-time = "2026-03-06T05:27:58.196Z" }, + { url = "https://files.pythonhosted.org/packages/af/26/8d288da55259592a9aff160af4192db56799a74d3389ce032f54c8c8b74c/wrapt-1.17.4rc1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dcf7b65ca203123c8613ae609441812b53ae047495e72b0dc423e5d31510128", size = 40586, upload-time = "2026-03-06T05:27:03.329Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d4/cd7b78cb59ea4d348a77906dfac3d30ed1c598732d9ee3cd8edcf7762bca/wrapt-1.17.4rc1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1713dac1faf01465058481dd07f7632847ca8867e77347527788aff0bdb32d8a", size = 108627, upload-time = "2026-03-06T05:27:28.884Z" }, + { url = "https://files.pythonhosted.org/packages/0b/26/6ae3790d46b56010f01dd74a207af7aebb7357b95487e222d1a6ad912f84/wrapt-1.17.4rc1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:094bef74a0ef4c04416775a4f1965b2a29847d6aafa935229c1bf9d18f1d8c58", size = 113179, upload-time = "2026-03-06T05:27:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0e/c0b0b05de9ebf705ca8daa1e86c20a244ce0862f08faf1b23784d3abf766/wrapt-1.17.4rc1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e1f828a32b4e71b6349a00a0a3bcc9e41413e0005160fb70601b83cb171ce6e", size = 103238, upload-time = "2026-03-06T05:21:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/ec/33/b2cd9f6b86bf322cb1711c6070b9efa6b28a8e8c063f56d165b30c8d1668/wrapt-1.17.4rc1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e24a05dae0ba49ce5f490bffc4e369a5770663c789c0bc862de8ac235b18394d", size = 110742, upload-time = "2026-03-06T05:27:46.881Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f4/b9709eea1e0087c8ccb1c7a38076a76ec3eb6f0555f74c3a65eaadf5c987/wrapt-1.17.4rc1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7d00e2453975a6519cbdde4812234ab0183860011aae2316acbad46f3b8e84e9", size = 102364, upload-time = "2026-03-06T05:27:51.288Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/bd4a00aef4d4b1a7eff25456b2f9c15de8ec9a3f4ccf98f0acdb2c48c879/wrapt-1.17.4rc1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89b5fe920975e4e63099aeb194b51ad0ad84b45995dada353aa1e5a551462fd0", size = 107013, upload-time = "2026-03-06T05:28:03.308Z" }, + { url = "https://files.pythonhosted.org/packages/86/73/aedee294890bde90b262b21156c20eb36450ee812a20384ea5df9ba49bbd/wrapt-1.17.4rc1-cp313-cp313t-win32.whl", hash = "sha256:c94efd8ca87b9333590b6ee0384a0863ad92b54646232396c3c8043b0d115d49", size = 38129, upload-time = "2026-03-06T05:27:12.074Z" }, + { url = "https://files.pythonhosted.org/packages/96/17/dbf146893d31705872d2e515cd2ef70e01e305aa441a1736cdeee856deb9/wrapt-1.17.4rc1-cp313-cp313t-win_amd64.whl", hash = "sha256:db3ea738ffd95b88a5874ed6c7d26ffad1b482a5b8036e7b4b667926d3d5d728", size = 40751, upload-time = "2026-03-06T05:27:18.843Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/2bafa54d3621ca0c8a0b7cd78150d6239e83553f8f2bf8e6fc17286bac34/wrapt-1.17.4rc1-cp313-cp313t-win_arm64.whl", hash = "sha256:d8f67707f553821691228bf3596bf60cf83e112c230ca4ebfae759feed20cf57", size = 38262, upload-time = "2026-03-06T05:26:54.324Z" }, + { url = "https://files.pythonhosted.org/packages/84/e2/203c4a94a4f2cb5bd1b2180261f213b6ecf386839d9c4a7b03b187e1d973/wrapt-1.17.4rc1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4384529d0f82bcdebec1d01f7b714b31ea34ee1b43a8399df5ed0db443bf6551", size = 39210, upload-time = "2026-03-06T05:21:13.2Z" }, + { url = "https://files.pythonhosted.org/packages/b9/de/0f3940df4cf001cc79cfd321c7e7856e6cdeac4c53b8292b4d318884a9be/wrapt-1.17.4rc1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d665e1f4bdeb551c55a56fe076f3da2aa4acea9b5108723adf4347b9af17bb70", size = 39339, upload-time = "2026-03-06T05:28:28.027Z" }, + { url = "https://files.pythonhosted.org/packages/28/87/1b13a950ad90919078951cadc8c8418241f55f6355bc1b64420072453d2f/wrapt-1.17.4rc1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95be0b13dcde68f73921026c66b4bb464a299683365a7243b5db49f220e5463f", size = 87262, upload-time = "2026-03-06T05:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/00/a9/c3015e3929b715ae2737eb332dc5e056bb0a3a450d26dca962dc93da8a32/wrapt-1.17.4rc1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7e86063ed1d5b46e2c6ac7c3c8c9bb1b47e47d3ceb804a93f566d1294810505", size = 88061, upload-time = "2026-03-06T05:27:33.243Z" }, + { url = "https://files.pythonhosted.org/packages/15/8f/83d676e926c2c6390e6019aacb3f598c929426d67d1d97d3ed26536a0ac9/wrapt-1.17.4rc1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c710707166eed80e37242d754a204f4c07b8f3ab8024b07d583f48024d260a05", size = 84543, upload-time = "2026-03-06T05:28:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/87/8d/f48862187bcee1d7d0a6c2c8cf4830ecd9e06bf0d770e6efbd2a78b70dad/wrapt-1.17.4rc1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c85cf9d6017e5188697a5947dd76f29ba1c56707ea612173b1b1ee1bc27b9601", size = 87050, upload-time = "2026-03-06T05:27:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/1e3c265902f02b3c1644568be86ddc3cf0d76552723ae71b7ca11e10bdc3/wrapt-1.17.4rc1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:44edeaf45e144c2de1102427530790c32eeb0084451f7816a58d744d077e0b3c", size = 83965, upload-time = "2026-03-06T05:27:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4c/24a7c0fa058212cb53a7f582c9631b1b9ce9d5a81400095c745a1cb7a4be/wrapt-1.17.4rc1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:201acefeff4fc6d497f411595c46f79eb91e562fa4883847db8148474a1e3d80", size = 86958, upload-time = "2026-03-06T05:28:24.737Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/445569dc31ee7a23c199afae532a41cc2f446d434d288e7544b1a38fbd19/wrapt-1.17.4rc1-cp314-cp314-win32.whl", hash = "sha256:73016054d0e32a65fa5da708e839be3036c786416adca00a0444aec5837b1b83", size = 37276, upload-time = "2026-03-06T05:28:21.776Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/1636a670886dec6c59fa60a8112fc3fd56c194b23b07106dbee465af73c2/wrapt-1.17.4rc1-cp314-cp314-win_amd64.whl", hash = "sha256:66b0485668cff7bfac0eaccccb3a991dba3f0d5205d6bc5a9c69aa120b2b6ccf", size = 39405, upload-time = "2026-03-06T05:26:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5e/9f820a1d60ea579b048a8486c319918fdf06b83cc37f67f8dd4c53b80df6/wrapt-1.17.4rc1-cp314-cp314-win_arm64.whl", hash = "sha256:2712e6caad2a5032d6496612eeca5cdb65fadd6da55c5f931d556ac656e3ebdd", size = 37367, upload-time = "2026-03-06T05:27:23.446Z" }, + { url = "https://files.pythonhosted.org/packages/14/92/617f98da4517f2bf2a63b1a929f5bec029292d6bd31c7fd79ee25d54635e/wrapt-1.17.4rc1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3102bbdc650a7e8fd8672e51c6d204688fc75257e2d3c6a12172a8e05c2ab0cd", size = 40565, upload-time = "2026-03-06T05:26:50.47Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/8c4444c471d90f9cfe1b453e5bf605fccadb2d3399d2ed60ed3240c188b3/wrapt-1.17.4rc1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a3ef8f9aad3593f3b00527da3815e15941caf169c51da5da18e64d1949da3f29", size = 40585, upload-time = "2026-03-06T05:21:14.419Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fb/c3938d7fef6ce445d32e5a757268adc4e5c298d1985dff95c535e1ceca38/wrapt-1.17.4rc1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:033b67f5cc44d992221617ea6be6f12d8857b90a5d0901738f4f6c92498d3298", size = 108671, upload-time = "2026-03-06T05:28:16.715Z" }, + { url = "https://files.pythonhosted.org/packages/ad/54/d5ae3c39c871ff63c973848558c1657fa09cf84c19e5242e25f57e8b251a/wrapt-1.17.4rc1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b3c400c7c7b6346e9d3d22f036443ff033fa924d472715d127f169e8f9e137", size = 113193, upload-time = "2026-03-06T05:27:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/18/c0/37f69e1231e8cfd3e642ff24f002cd71cbe477fca2abe6ec43978426f09a/wrapt-1.17.4rc1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b3a9ed0f966b6a199e251800f5ee895bb41694ad1bb92f19446cbb90e68cdec", size = 103256, upload-time = "2026-03-06T05:27:52.645Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5b/71f5f63bb3c4bfa909ae320ebcf290250cd86207d54cdffc3b12c1a57b8a/wrapt-1.17.4rc1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:076702de22f5df07bfaeb67ac750aabe2167fd703ed60ac8e2edb42a082119e8", size = 110756, upload-time = "2026-03-06T05:26:59.375Z" }, + { url = "https://files.pythonhosted.org/packages/fe/52/6ef9887520e0038cacb97bfd4375a83e3cf947d82a11e4017af2a98647cb/wrapt-1.17.4rc1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:1374e2051eff90875b3331dc5930209807db9e03ba863c2a9009ab7ba77daa7c", size = 102369, upload-time = "2026-03-06T05:26:52.912Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/670237dcee12fb293cb4674f93db112806783a33cc8cc18fa64214c12614/wrapt-1.17.4rc1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a02b14dfc3ded8f1be82d824628ccda63ac37d1833c8328adf7a6b019f6a230", size = 107045, upload-time = "2026-03-06T05:27:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/1a/15/2ecc4112171d195ff1c4f0baf7d345ca5f0ec464381bc7024857b3db47d5/wrapt-1.17.4rc1-cp314-cp314t-win32.whl", hash = "sha256:2bdf836e6c8e8f26c85716c08a0063309a2d9362e090b499f32fc4de8f2c651d", size = 38809, upload-time = "2026-03-06T05:21:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/d7/45/81fec744e8c88f6255a5ccc317997a01b1a08fa925b211e2078fa8bfbddf/wrapt-1.17.4rc1-cp314-cp314t-win_amd64.whl", hash = "sha256:f75df0a7f1dab354cd092ee9c466efb3556f87ecf103683cecc0f7488e9dbf77", size = 41427, upload-time = "2026-03-06T05:28:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/72/d6ecf86cb5f3574a55fd2ba58c6eca447bee90a8757f1f32fba4b14ff9d5/wrapt-1.17.4rc1-cp314-cp314t-win_arm64.whl", hash = "sha256:3e2f5e602d656b53118bfdc9d5d94b840069f1753923e48726f0bc02dd65deb8", size = 38531, upload-time = "2026-03-06T05:27:57.157Z" }, + { url = "https://files.pythonhosted.org/packages/29/b2/367cc462b6ad84bfb7a93b00f5c4b01c7bc880a0e7ce36c1a3900eee153a/wrapt-1.17.4rc1-py3-none-any.whl", hash = "sha256:9cc3fb27bc5f564895c967b9b06dd2b799ee107b33a7f8ad8b8346b5d6b35b60", size = 23719, upload-time = "2026-03-06T05:27:55.715Z" }, ] [[package]] From c383cca6de1b702e97dbb1d64b43027166b2d9ea Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:26:30 +0300 Subject: [PATCH 80/81] ci(workflows): replace postgres with pgvector --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f4e3bd..06b0b36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: services: postgres: - image: postgres:16 + image: pgvector/pgvector:pg16 env: POSTGRES_DB: ai_notes POSTGRES_USER: postgres From 9d8acaf9d97bccd3a1655b980342cf712ce94b0a Mon Sep 17 00:00:00 2001 From: NKTKLN Date: Thu, 25 Jun 2026 11:34:20 +0300 Subject: [PATCH 81/81] test(pgvector): fix pgvector plugin --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 5dc623c..b66a6fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from collections.abc import AsyncIterator import pytest_asyncio +from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from ai_notes_api.core import settings @@ -23,6 +24,7 @@ async def async_session() -> AsyncIterator[AsyncSession]: engine = create_async_engine(settings.database_url) async with engine.begin() as conn: + await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all)