From ca8c94b38e10c4e9cc886c050e2513f43aa06ecd Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Sun, 6 Apr 2025 01:16:35 +0000 Subject: [PATCH 01/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Add=20do?= =?UTF-8?q?cument=20and=20model=20CRUD=20operations,=20enhance=20data=20ha?= =?UTF-8?q?ndling=20and=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhances document and model data handling Adds CRUD operations for documents and models to improve data management. Improves data validation and caching mechanisms for predictions. Registers models in the database and updates document extraction logic. --- .../routers/anonymizer/anonymizer.py | 248 ++++++++++++------ .../routers/misc/document_extract.py | 41 ++- aymurai/api/main.py | 25 +- aymurai/database/crud/document.py | 71 +++++ aymurai/database/crud/model.py | 28 ++ aymurai/database/crud/prediction.py | 73 ++++++ aymurai/database/meta/document.py | 38 +++ aymurai/database/meta/model.py | 53 ++++ aymurai/database/meta/paragraph.py | 57 ++++ aymurai/database/meta/prediction.py | 62 +++++ aymurai/database/schema.py | 14 + aymurai/database/utils.py | 15 +- ...ase.py => b09c785624d8_create_database.py} | 86 +++++- aymurai/meta/api_interfaces.py | 10 +- aymurai/settings.py | 10 + 15 files changed, 710 insertions(+), 121 deletions(-) create mode 100644 aymurai/database/crud/document.py create mode 100644 aymurai/database/crud/model.py create mode 100644 aymurai/database/crud/prediction.py create mode 100644 aymurai/database/meta/document.py create mode 100644 aymurai/database/meta/model.py create mode 100644 aymurai/database/meta/paragraph.py create mode 100644 aymurai/database/meta/prediction.py rename aymurai/database/versions/{c371aff13e1e_create_database.py => b09c785624d8_create_database.py} (81%) diff --git a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py index 6f9db64..3bd06ac 100644 --- a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py +++ b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py @@ -1,36 +1,36 @@ -import json import os import subprocess import tempfile +from functools import cache from threading import Lock import torch -from fastapi import Body, Depends, Form, Query, UploadFile +from fastapi import Body, Depends, Query from fastapi.responses import FileResponse from fastapi.routing import APIRouter +from pydantic import UUID5 from sqlmodel import Session from starlette.background import BackgroundTask from aymurai.api.utils import load_pipeline -from aymurai.database.crud.anonymization.document import anonymization_document_create -from aymurai.database.crud.anonymization.paragraph import ( - anonymization_paragraph_batch_create_update, - anonymization_paragraph_create, - anonymization_paragraph_read, +from aymurai.database.crud.model import register_model +from aymurai.database.crud.prediction import read_prediction, read_validation +from aymurai.database.schema import ( + Document, + Model, + Prediction, + PredictionCreate, ) -from aymurai.database.schema import AnonymizationParagraph from aymurai.database.session import get_session -from aymurai.database.utils import data_to_uuid, text_to_uuid +from aymurai.database.utils import text_to_uuid from aymurai.logger import get_logger from aymurai.meta.api_interfaces import ( DocLabel, - DocumentAnnotations, DocumentInformation, TextRequest, ) from aymurai.settings import settings from aymurai.text.anonymization import DocAnonymizer -from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER from aymurai.utils.misc import get_element logger = get_logger(__name__) @@ -44,6 +44,24 @@ router = APIRouter() +@cache +def get_model(session: Session = Depends(get_session)) -> Model: + """ + Get the model from the database. + + Args: + session (Session): Database session dependency. + + Returns: + Model: The model from the database. + """ + return register_model( + model_name="flair-anonymizer", + app_version=settings.APP_VERSION, + session=session, + ) + + # MARK: Predict @router.post("/predict", response_model=DocumentInformation) async def anonymizer_paragraph_predict( @@ -54,6 +72,7 @@ async def anonymizer_paragraph_predict( True, description="Use cache to store or retrive predictions" ), session: Session = Depends(get_session), + model: Model = Depends(get_model), ) -> DocumentInformation: """ Endpoint to predict anonymization for a given paragraph of text. @@ -67,19 +86,29 @@ async def anonymizer_paragraph_predict( DocumentInformation: The anonymized document information including the text and labels. """ - logger.info("anonymization predict single") + logger.info(f"Running prediction: {model.name} ({model.version})") logger.info(f"Checking cache (use cache: {use_cache})") - text = text_request.text - paragraph_id = text_to_uuid(text) - - cached_prediction = session.get(AnonymizationParagraph, paragraph_id) + input_text = text_request.text + prediction_id = text_to_uuid(f"{input_text}-{model.id}") + + ################################################################################## + # Load from cache + ################################################################################## + cached_prediction = read_prediction( + text=input_text, model_id=model.id, session=session + ) if cached_prediction and use_cache: - logger.info(f"cache loaded from key: {paragraph_id}") + logger.info(f"cache loaded from prediction: {cached_prediction.id}") logger.debug(f"{cached_prediction}") - labels = cached_prediction.prediction - return DocumentInformation(document=cached_prediction.text, labels=labels or []) + return DocumentInformation( + document=cached_prediction.input, labels=cached_prediction.prediction + ) + + ################################################################################## + # Predict + ################################################################################## logger.info("Running prediction") item = [{"path": "empty", "data": {"doc.text": text_request.text}}] @@ -92,19 +121,45 @@ async def anonymizer_paragraph_predict( processed = pipeline.predict_single(processed[0]) processed = pipeline.postprocess([processed]) - text = get_element(processed[0], ["data", "doc.text"]) or "" - labels = get_element(processed[0], ["predictions", "entities"]) or [] + output_text = get_element(processed[0], ["data", "doc.text"]) or "" + output_labels = get_element(processed[0], ["predictions", "entities"]) or [] - if use_cache: - logger.info(f"saving in cache: {paragraph_id}") - paragraph = AnonymizationParagraph( - id=paragraph_id, - text=text, - prediction=labels, + ################################################################################## + # Sanity check + ################################################################################## + if input_text != output_text: + logger.critical( + f"Input and output text do not match: {input_text} != {output_text}" ) - paragraph = anonymization_paragraph_create(paragraph, session=session) + if settings.ERROR_HANDLER == "raise": + raise ValueError("Input and output text do not match") + + # Run validation on the output labels + output_labels = [ + DocLabel(**label).model_dump(mode="json") for label in output_labels + ] + + ################################################################################# + # Save to cache + ################################################################################# + logger.info(f"saving in cache: {prediction_id}") + pred = session.get(Prediction, prediction_id) + if pred: + logger.warning(f"Prediction already exists: {prediction_id}. Updating.") + pred.input = output_text + pred.prediction = output_labels + else: + pred = PredictionCreate( + input=output_text, + prediction=output_labels, + fk_model=model.id, + ).compile() - return DocumentInformation(document=text, labels=paragraph.prediction) + session.add(pred) + session.commit() + session.refresh(pred) + + return DocumentInformation(document=pred.input, labels=pred.prediction) # MARK: Validate @@ -114,6 +169,7 @@ async def anonymizer_get_paragraph_validation( {"text": "Acusado: Ramiro Marrón DNI 34.555.666."} ), session: Session = Depends(get_session), + model: Model = Depends(get_model), ) -> list[DocLabel] | None: """ Get the validation labels for a given paragraph text. @@ -127,21 +183,23 @@ async def anonymizer_get_paragraph_validation( """ text = text_request.text - paragraph_id = text_to_uuid(text) - paragraph = anonymization_paragraph_read(paragraph_id, session=session) - if not paragraph: + logger.info(f"Checking validation: {model.name} ({model.version})") + pred = read_validation(input_hash=text, model_name=model.name, session=session) + + if not pred: return None - return paragraph.validation + return pred.validation # MARK: Document Compilation @router.post("/anonymize-document") async def anonymizer_compile_document( - file: UploadFile, - annotations: str = Form(...), + document_id: UUID5, + annotations: list[DocumentInformation], session: Session = Depends(get_session), + model: Model = Depends(get_model), ) -> FileResponse: """ Compile Anonimized document from original file and annotations @@ -153,63 +211,101 @@ async def anonymizer_compile_document( Returns: FileResponse: Anonymized document """ - logger.info(f"receiving => {file.filename}") - extension = MIMETYPE_EXTENSION_MAPPER.get(file.content_type) - logger.info(f"detection extension: {extension} ({file.content_type})") + document = session.get(Document, document_id) + + ################################################################################## + # Sanity check + ################################################################################## + # Check if the document exists + if not document: + raise ValueError(f"Document not found: {document_id}") + + # Check length of annotations and document match + if len(annotations) != len(document.paragraphs): + raise ValueError( + f"The number of annotated paragraphs ({len(annotations)}) does not" + f" match the number of paragraphs in the document" + f" ({len(document.paragraphs)})" + ) + + # Check if the annotated paragraphs match the document paragraphs one by one + for i, (annot_paragraph, doc_paragraph) in enumerate( + zip(annotations, document.paragraphs) + ): + if annot_paragraph.document != doc_paragraph.text: + msg = ( + f"The annotated paragraph ({annot_paragraph.document}) does not match" + f" the document paragraph ({doc_paragraph.text})" + ) + if settings.ERROR_HANDLER == "raise": + raise ValueError(msg) + logger.warning(msg) + + #################################################################################### + # Updating validations + # #################################################################################### + + predictions = [] + for paragraph in annotations: + with session.no_autoflush: + prediction = read_prediction( + text=paragraph.document, model_id=model.id, session=session + ) + if prediction: + prediction.validation = paragraph.labels + predictions.append(prediction) + else: + logger.warning("Prediction not found.") + if settings.ERROR_HANDLER == "raise": + raise ValueError( + f"Prediction not found for paragraph: `{paragraph.document}`" + ) + prediction = Prediction( + input=paragraph.document, + fk_model=model.id, + validation=paragraph.labels, + ) + predictions.append(prediction) + + session.add_all(predictions) + session.commit() + for prediction in predictions: + session.refresh(prediction) + + #################################################################################### # Create a temporary file - _, suffix = os.path.splitext(file.filename) + #################################################################################### + + filename = document.name + logger.info(f"Document found: {document_id} ({filename})") + + # Create a temporary file + _, suffix = os.path.splitext(filename) suffix = suffix if suffix == ".docx" else ".txt" tmp_dir = tempfile.gettempdir() # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed # This is necessary on Windows, as the file is locked by the file object and cannot be deleted - with tempfile.NamedTemporaryFile( - suffix=suffix, delete=False, dir=tmp_dir - ) as tmp_file: - tmp_filename = tmp_file.name + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, dir=tmp_dir) as file: + tmp_filename = file.name logger.info(f"saving temp file on local storage => {tmp_filename}") - data = file.file.read() - tmp_file.write(data) - tmp_file.flush() - tmp_file.close() + file.write(document.data) + file.flush() + file.close() logger.info(f"saved temp file on local storage => {tmp_filename}") - annots_json = json.loads(annotations) - annots = DocumentAnnotations.model_validate(annots_json) - logger.info(f"processing annotations => {annots}") - - # Add paragraphs to the database - # validation MUST be at least an empty list, to remember user feedback - paragraphs = [ - AnonymizationParagraph( - id=text_to_uuid(paragraph.document), - text=paragraph.document, - validation=paragraph.labels or [], - ) - for paragraph in annots.data - ] - paragraphs = anonymization_paragraph_batch_create_update( - paragraphs, session=session - ) - - anonymization_document_create( - id=data_to_uuid(data), - name=file.filename, - paragraphs=paragraphs, - session=session, - override=True, - ) - + #################################################################################### # Anonymize the document + #################################################################################### doc_anonymizer = DocAnonymizer() if suffix == ".docx": item = {"path": tmp_filename} doc_anonymizer( item, - [document_information.model_dump() for document_information in annots.data], + [document_information.model_dump() for document_information in annotations], tmp_dir, ) logger.info(f"saved temp file on local storage => {tmp_filename}") @@ -220,7 +316,7 @@ async def anonymizer_compile_document( doc_anonymizer.replace_labels_in_text(document_information.model_dump()) .replace("<", "<") .replace(">", ">") - for document_information in annots.data + for document_information in annotations ] with open(tmp_filename, "w") as f: f.write("\n".join(anonymized_doc)) @@ -261,5 +357,5 @@ async def anonymizer_compile_document( odt, background=BackgroundTask(os.remove, odt), media_type="application/octet-stream", - filename=f"{os.path.splitext(file.filename)[0]}.odt", + filename=f"{os.path.splitext(filename)[0]}.odt", ) diff --git a/aymurai/api/endpoints/routers/misc/document_extract.py b/aymurai/api/endpoints/routers/misc/document_extract.py index d1ba94a..86967bd 100644 --- a/aymurai/api/endpoints/routers/misc/document_extract.py +++ b/aymurai/api/endpoints/routers/misc/document_extract.py @@ -5,11 +5,14 @@ from fastapi import Depends, UploadFile from fastapi.routing import APIRouter from more_itertools import unique_justseen +from sqlmodel import Session from aymurai.api.utils import get_pipeline_doc_extract +from aymurai.database.schema import Paragraph, Document, DocumentPublic +from aymurai.database.session import get_session from aymurai.database.utils import data_to_uuid from aymurai.logger import get_logger -from aymurai.meta.api_interfaces import Document + from aymurai.pipeline import AymurAIPipeline from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER from aymurai.utils.misc import get_element @@ -20,16 +23,24 @@ router = APIRouter() -@router.post("/document-extract", response_model=Document) +@router.post("/document-extract", response_model=DocumentPublic) def plain_text_extractor( file: UploadFile, pipeline: AymurAIPipeline = Depends(get_pipeline_doc_extract), -) -> Document: + session: Session = Depends(get_session), + use_cache: bool = True, +) -> DocumentPublic: logger.info(f"receiving => {file.filename}") extension = MIMETYPE_EXTENSION_MAPPER.get(file.content_type) logger.info(f"detected extension: {extension} ({file.content_type})") data = file.file.read() + document_id = data_to_uuid(data) + + document = session.get(Document, document_id) + if document and use_cache: + logger.warning(f"Document already exists: {document_id}. skipping creation.") + return document # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed # This is necessary on Windows, as the file is locked by the file object and cannot be deleted @@ -57,10 +68,26 @@ def plain_text_extractor( os.remove(tmp_filename) logger.info(f"removed temp file from local storage => {tmp_filename}") - document_id = data_to_uuid(data) doc_text: str = get_element(processed[0], ["data", "doc.text"], "") - document = [text.strip() for text in doc_text.split("\n") if text.strip()] - document = list(unique_justseen(document)) + paragraph_text = [text.strip() for text in doc_text.split("\n") if text.strip()] + paragraph_text = list(unique_justseen(paragraph_text)) + + # Add paragraphs to the database + # validation MUST be at least an empty list, to remember user feedback + paragraphs = [ + Paragraph(text=paragraph, order=i) for i, paragraph in enumerate(paragraph_text) + ] + + document = Document( + id=document_id, + data=data, + name=file.filename, + paragraphs=paragraphs, + ) + + session.add_all([document] + paragraphs) + session.commit() + session.refresh(document) - return Document(document=document, document_id=document_id) + return document diff --git a/aymurai/api/main.py b/aymurai/api/main.py index 63e3881..c9b788a 100644 --- a/aymurai/api/main.py +++ b/aymurai/api/main.py @@ -7,7 +7,6 @@ from alembic.config import Config from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles -from fastapi.openapi.utils import get_openapi from fastapi.responses import RedirectResponse from fastapi.middleware.cors import CORSMiddleware @@ -17,10 +16,6 @@ from aymurai.pipeline import AymurAIPipeline from aymurai.api.startup.database import check_db_connection -try: - from aymurai.version import __version__ -except ImportError: - __version__ = "0.0.0" logger = get_logger(__name__) @@ -47,7 +42,7 @@ async def lifespan(app: FastAPI): api = FastAPI( title="AymurAI API", - version=__version__, + version=settings.APP_VERSION, lifespan=lifespan, ) @@ -61,7 +56,7 @@ async def lifespan(app: FastAPI): ) -logger.info("Loading server ...") +logger.info(f"Loading server (Aymurai API - {settings.APP_VERSION})...") logger.info(f"CORS_ORIGINS: {settings.CORS_ORIGINS}") @@ -86,22 +81,6 @@ async def index(): ) -def custom_openapi(): - if api.openapi_schema: - return api.openapi_schema - openapi_schema = get_openapi( - title="AymurAI API - Swagger UI", - version="0.5.0", - description="", - routes=api.routes, - ) - openapi_schema["info"]["x-logo"] = {"url": "static/logo256-text.ico"} - api.openapi_schema = openapi_schema - return api.openapi_schema - - -api.openapi = custom_openapi - ################################################################################ # MARK: API ENDPOINTS ################################################################################ diff --git a/aymurai/database/crud/document.py b/aymurai/database/crud/document.py new file mode 100644 index 0000000..cc97a1c --- /dev/null +++ b/aymurai/database/crud/document.py @@ -0,0 +1,71 @@ +import uuid + +from sqlmodel import Session, select + +from aymurai.logger import get_logger +from aymurai.database.schema import ( + Document, + Paragraph, + DocumentUpdate, +) + +logger = get_logger(__name__) + + +def document_create( + id: uuid.UUID, + name: str, + paragraphs: list[Paragraph], + session: Session, +) -> Document: + document = Document(id=id, name=name, paragraphs=paragraphs) + + exists = session.get(Document, id) + if exists: + logger.warning(f"Document already exists: {id}. skipping creation.") + return exists + + session.add(document) + session.commit() + session.refresh(document) + + return document + + +def document_read( + document_id: uuid.UUID, + session: Session, +) -> Document | None: + statement = select(Document).where(Document.id == document_id) + data = session.exec(statement).first() + return data + + +def document_update( + document_id: uuid.UUID, + document_in: DocumentUpdate, + session: Session, +) -> Document: + statement = select(Document).where(Document.id == document_id) + document = session.exec(statement).first() + + if not document: + raise ValueError(f"Document not found: {document_id}") + + for field, value in document_in.model_dump(exclude_unset=True).items(): + setattr(document, field, value) + + return document_create(document, session) + + +def document_delete(document_id: uuid.UUID, session: Session): + statement = select(Document).where(Document.id == document_id) + document = session.exec(statement).first() + + if not document: + raise ValueError(f"Document not found: {document_id}") + + session.delete(document) + session.commit() + + return diff --git a/aymurai/database/crud/model.py b/aymurai/database/crud/model.py new file mode 100644 index 0000000..7a39037 --- /dev/null +++ b/aymurai/database/crud/model.py @@ -0,0 +1,28 @@ +from sqlmodel import Session +from aymurai.database.schema import Model, ModelPublic, ModelCreate +from aymurai.database.utils import text_to_uuid +from aymurai.logger import get_logger + + +logger = get_logger(__name__) + + +def register_model(model_name: str, app_version: str, session: Session) -> ModelPublic: + """ + Register a model in the database. + """ + model_id = text_to_uuid(f"{model_name}-{app_version}") + + # Check if the model already exists + model = session.get(Model, model_id) + if model: + logger.warning(f"Model already exists: {model_id}. skipping creation.") + return ModelPublic(**model.model_dump()) + + model = ModelCreate(name=model_name, version=app_version).compile() + + session.add(model) + session.commit() + session.refresh(model) + + return ModelPublic(**model.model_dump()) diff --git a/aymurai/database/crud/prediction.py b/aymurai/database/crud/prediction.py new file mode 100644 index 0000000..d2993a4 --- /dev/null +++ b/aymurai/database/crud/prediction.py @@ -0,0 +1,73 @@ +import uuid +from sqlmodel import Session, select +from aymurai.database.schema import Prediction + +from aymurai.database.utils import text_to_hash +from aymurai.database.schema import Model + + +def read_validation( + text: str, + model_name: str, + session: Session, +) -> Prediction | None: + """ + Read the validation labels for a given input hash. + + Args: + input_hash (str): The hash of the input text. + model_name (str): The name of the model associated with the prediction. + session (Session): Database session dependency. + + Returns: + list[DocLabel] | None: A list of validation labels for the given input hash, or None if no validation exists. + """ + + input_hash = text_to_hash(text) + + statement = ( + select(Prediction) + .join(Model, Prediction.fk_model == Model.id) + .where( + Prediction.input_hash == input_hash, + Model.name == model_name, + Prediction.validation.isnot(None), + ) + .order_by(Prediction.updated_at.desc(), Prediction.created_at.desc()) + ) + pred = session.exec(statement).first() + + return pred or None + + +def read_prediction( + text: str, + model_id: uuid.UUID, + session: Session, +) -> Prediction | None: + """ + Read the validation labels for a given input hash. + + Args: + input_hash (str): The hash of the input text. + model_name (str): The name of the model associated with the prediction. + session (Session): Database session dependency. + + Returns: + list[DocLabel] | None: A list of validation labels for the given input hash, or None if no validation exists. + """ + + input_hash = text_to_hash(text) + + statement = ( + select(Prediction) + .where( + Prediction.input_hash == input_hash, + Prediction.fk_model == model_id, + # Prediction.prediction.isnot(None), + ) + .order_by(Prediction.updated_at.desc(), Prediction.created_at.desc()) + ) + pred = session.exec(statement).first() + + return pred or None diff --git a/aymurai/database/meta/document.py b/aymurai/database/meta/document.py new file mode 100644 index 0000000..a56c409 --- /dev/null +++ b/aymurai/database/meta/document.py @@ -0,0 +1,38 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel +from sqlmodel import Field, SQLModel, Relationship +from sqlalchemy import Column, DateTime, func, text, LargeBinary + +from aymurai.database.meta.paragraph import Paragraph, ParagraphPublic + + +class Document(SQLModel, table=True): + __tablename__ = "document" + + id: uuid.UUID | None = Field(None, primary_key=True) + data: bytes = Field( + sa_column=Column(LargeBinary), + description="binary data of the document", + ) + created_at: datetime = Field( + sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} + ) + updated_at: datetime | None = Field( + sa_column=Column(DateTime(), onupdate=func.now()) + ) + + name: str = Field(nullable=False) + + paragraphs: list["Paragraph"] = Relationship(back_populates="document") + + +class DocumentUpdate(BaseModel): + name: str | None = None + + +class DocumentPublic(BaseModel): + id: uuid.UUID + name: str + paragraphs: list[ParagraphPublic] | None = None diff --git a/aymurai/database/meta/model.py b/aymurai/database/meta/model.py new file mode 100644 index 0000000..1031f54 --- /dev/null +++ b/aymurai/database/meta/model.py @@ -0,0 +1,53 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel, model_validator +from sqlalchemy import text +from sqlmodel import Field, SQLModel, Relationship +from aymurai.database.utils import text_to_uuid + +from typing_extensions import TYPE_CHECKING + +if TYPE_CHECKING: + from aymurai.database.meta.prediction import Prediction + + +class ModelBase(SQLModel): + id: uuid.UUID | None = Field(None, primary_key=True) + + name: str = Field(nullable=False) + version: str = Field(nullable=False) + + @model_validator(mode="after") + def validate_name_version(self) -> "ModelBase": + self.id = text_to_uuid(f"{self.name}-{self.version}") + return self + + +class Model(ModelBase, table=True): + __tablename__ = "model" + + created_at: datetime = Field( + sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} + ) + + predictions: list["Prediction"] = Relationship(back_populates="model") + + # @model_validator(mode="before") + # def generate_id(cls, values: dict): + # if values.get("id") is None and set(values.keys()) & {"name", "version"}: + # values["id"] = text_to_uuid(f"{values['name']}-{values['version']}") + # return values + + +class ModelCreate(ModelBase): + def compile(self) -> Model: + return Model(**self.model_dump()) + + +class ModelPublic(BaseModel): + id: uuid.UUID + name: str + version: str + + created_at: datetime diff --git a/aymurai/database/meta/paragraph.py b/aymurai/database/meta/paragraph.py new file mode 100644 index 0000000..7bac2bd --- /dev/null +++ b/aymurai/database/meta/paragraph.py @@ -0,0 +1,57 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel, model_validator, computed_field +from typing_extensions import TYPE_CHECKING, Self +from sqlmodel import Field, SQLModel, Relationship +from sqlalchemy import Column, DateTime, func, text + +from aymurai.database.utils import text_to_uuid + +if TYPE_CHECKING: + from aymurai.database.meta.document import Document + + +class ParagraphBase(SQLModel): + id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) + + text: str = Field(nullable=False) + + @model_validator(mode="after") + def validate_text(self) -> Self: + self.id = text_to_uuid(self.text) + return self + + @computed_field + def hash(self) -> uuid.UUID: + """Compute the hash of the text""" + return text_to_uuid(self.text) + + +class Paragraph(ParagraphBase, table=True): + __tablename__ = "paragraph" + + # NOTE: SQLModel with table=True does not run validators + created_at: datetime = Field( + sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} + ) + updated_at: datetime | None = Field( + sa_column=Column(DateTime(), onupdate=func.now()) + ) + + fk_document: uuid.UUID = Field(None, foreign_key="document.id") + document: "Document" = Relationship(back_populates="paragraphs") + + +class ParagraphUpdate(BaseModel): + text: str | None = None + + +class ParagraphPublic(BaseModel): + id: uuid.UUID + + text: str + hash: uuid.UUID + + created_at: datetime + updated_at: datetime | None diff --git a/aymurai/database/meta/prediction.py b/aymurai/database/meta/prediction.py new file mode 100644 index 0000000..de0f1ef --- /dev/null +++ b/aymurai/database/meta/prediction.py @@ -0,0 +1,62 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel, model_validator +from sqlmodel import Field, SQLModel, Relationship +from sqlalchemy import JSON, Column, DateTime, func, text + +from aymurai.meta.api_interfaces import DocLabel + +from aymurai.database.meta.model import Model, ModelPublic +from aymurai.database.utils import text_to_hash + + +class PredictionBase(SQLModel): + id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) + + input: str = Field(nullable=False) + input_hash: str | None = None + + prediction: list[DocLabel] | None = Field(None, sa_column=Column(JSON)) + validation: list[DocLabel] | None = Field(None, sa_column=Column(JSON)) + + fk_model: uuid.UUID = Field(foreign_key="model.id") + + @model_validator(mode="after") + def validate_input(self) -> "PredictionBase": + self.input_hash = text_to_hash(self.input) + return self + + +class Prediction(PredictionBase, table=True): + __tablename__ = "prediction" + + input_hash: str + + created_at: datetime = Field( + sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} + ) + updated_at: datetime | None = Field( + sa_column=Column(DateTime(), onupdate=func.now()) + ) + + model: "Model" = Relationship(back_populates="predictions") + + +class PredictionCreate(PredictionBase): + def compile(self) -> Prediction: + return Prediction(**self.model_dump()) + + +class PredictionPublic(BaseModel): + """Datatype for a prediction""" + + id: uuid.UUID + created_at: datetime + updated_at: datetime | None = None + + input: str + prediction: list[DocLabel] = Field(default_factory=list) + validation: list[DocLabel] = Field(default_factory=list) + + model: ModelPublic diff --git a/aymurai/database/schema.py b/aymurai/database/schema.py index 2337a9d..c040f74 100644 --- a/aymurai/database/schema.py +++ b/aymurai/database/schema.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 from .meta.datapublic.dataset import ( DataPublicDataset, DataPublicDatasetRead, @@ -30,3 +31,16 @@ AnonymizationDocumentUpdate, AnonymizationDocumentParagraph, ) + +from .meta.document import ( + Document, + DocumentPublic, + DocumentUpdate, +) +from .meta.paragraph import ( + Paragraph, + ParagraphPublic, + ParagraphUpdate, +) +from .meta.model import Model, ModelPublic, ModelCreate +from .meta.prediction import Prediction, PredictionPublic, PredictionCreate diff --git a/aymurai/database/utils.py b/aymurai/database/utils.py index b692359..97e806b 100644 --- a/aymurai/database/utils.py +++ b/aymurai/database/utils.py @@ -1,13 +1,24 @@ import uuid from hashlib import blake2b +from functools import cache +from pydantic import UUID5 -def data_to_uuid(data: bytes) -> uuid.UUID: + +@cache +def data_to_uuid(data: bytes) -> UUID5: h = blake2b(digest_size=16) h.update(data) return uuid.uuid5(uuid.NAMESPACE_DNS, h.hexdigest()) -def text_to_uuid(text: str) -> uuid.UUID: +def text_to_uuid(text: str) -> UUID5: return data_to_uuid(text.encode("utf-8")) + + +def text_to_hash(text: str) -> str: + h = blake2b(digest_size=16) + h.update(text.encode("utf-8")) + + return h.hexdigest() diff --git a/aymurai/database/versions/c371aff13e1e_create_database.py b/aymurai/database/versions/b09c785624d8_create_database.py similarity index 81% rename from aymurai/database/versions/c371aff13e1e_create_database.py rename to aymurai/database/versions/b09c785624d8_create_database.py index b841a89..d33f06a 100644 --- a/aymurai/database/versions/c371aff13e1e_create_database.py +++ b/aymurai/database/versions/b09c785624d8_create_database.py @@ -1,19 +1,19 @@ """Create database -Revision ID: c371aff13e1e -Revises: -Create Date: 2025-02-01 02:41:38.595881 +Revision ID: b09c785624d8 +Revises: +Create Date: 2025-04-05 20:28:12.090153 """ +from typing import Sequence, Union -from typing import Union, Sequence - -import sqlmodel -import sqlalchemy as sa from alembic import op +import sqlalchemy as sa +import sqlmodel + # revision identifiers, used by Alembic. -revision: str = "c371aff13e1e" +revision: str = "b09c785624d8" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -77,6 +77,33 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint("id"), ) + op.create_table( + "document", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("data", sa.LargeBinary(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "model", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( "anonymization_document_paragraph", sa.Column("id", sa.Uuid(), nullable=False), @@ -309,14 +336,57 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id", "document_id", "paragraph_id"), ) + op.create_table( + "paragraph", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("fk_document", sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint( + ["fk_document"], + ["document.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "prediction", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("input", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("prediction", sa.JSON(), nullable=True), + sa.Column("validation", sa.JSON(), nullable=True), + sa.Column("fk_model", sa.Uuid(), nullable=False), + sa.Column("input_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["fk_model"], + ["model.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("prediction") + op.drop_table("paragraph") op.drop_table("datapublic_document_paragraph") op.drop_table("datapublic_dataset") op.drop_table("anonymization_document_paragraph") + op.drop_table("model") + op.drop_table("document") op.drop_table("datapublic_paragraph") op.drop_table("datapublic_document") op.drop_table("anonymization_paragraph") diff --git a/aymurai/meta/api_interfaces.py b/aymurai/meta/api_interfaces.py index 0a6beee..5c01b55 100644 --- a/aymurai/meta/api_interfaces.py +++ b/aymurai/meta/api_interfaces.py @@ -48,8 +48,8 @@ class DocumentAnnotations(BaseModel): data: list[DocumentInformation] -class Document(BaseModel): - document: list[str] - document_id: UUID5 - header: list[str] | None = None - footer: list[str] | None = None +# class Document(BaseModel): +# document: list[str] +# document_id: UUID5 +# header: list[str] | None = None +# footer: list[str] | None = None diff --git a/aymurai/settings.py b/aymurai/settings.py index 9fdd533..6e6ebc3 100644 --- a/aymurai/settings.py +++ b/aymurai/settings.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from typing import Literal from dotenv import load_dotenv from pydantic_settings import BaseSettings @@ -7,6 +8,11 @@ import aymurai +try: + from aymurai.version import __version__ +except ImportError: + __version__ = "0.0.0" + PARENT = Path(aymurai.__file__).parent @@ -24,6 +30,10 @@ def load_env(): class Settings(BaseSettings): model_config = ConfigDict(case_sensitive=True) + APP_VERSION: str = __version__ + + ERROR_HANDLER: Literal["ignore", "raise"] = "ignore" + CORS_ORIGINS: list[str] | str = ",".join( [ "http://localhost", From 0b6a594379cd9812fa321c6527c0885d30c6f82d Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Sun, 6 Apr 2025 03:04:33 +0000 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=94=A7=20refactor:=20Simplify=20pre?= =?UTF-8?q?diction=20reading=20and=20update=20relationships=20in=20Paragra?= =?UTF-8?q?ph=20and=20Prediction=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/endpoints/routers/anonymizer/anonymizer.py | 7 +++---- aymurai/database/meta/paragraph.py | 4 +++- aymurai/database/meta/prediction.py | 8 +++++++- ...te_database.py => 6a418ffd84da_create_database.py} | 11 ++++++++--- 4 files changed, 21 insertions(+), 9 deletions(-) rename aymurai/database/versions/{b09c785624d8_create_database.py => 6a418ffd84da_create_database.py} (98%) diff --git a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py index 3bd06ac..3b2ebfe 100644 --- a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py +++ b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py @@ -248,10 +248,9 @@ async def anonymizer_compile_document( predictions = [] for paragraph in annotations: - with session.no_autoflush: - prediction = read_prediction( - text=paragraph.document, model_id=model.id, session=session - ) + prediction = read_prediction( + text=paragraph.document, model_id=model.id, session=session + ) if prediction: prediction.validation = paragraph.labels predictions.append(prediction) diff --git a/aymurai/database/meta/paragraph.py b/aymurai/database/meta/paragraph.py index 7bac2bd..307e409 100644 --- a/aymurai/database/meta/paragraph.py +++ b/aymurai/database/meta/paragraph.py @@ -9,7 +9,7 @@ from aymurai.database.utils import text_to_uuid if TYPE_CHECKING: - from aymurai.database.meta.document import Document + from aymurai.database.schema import Document, Prediction class ParagraphBase(SQLModel): @@ -42,6 +42,8 @@ class Paragraph(ParagraphBase, table=True): fk_document: uuid.UUID = Field(None, foreign_key="document.id") document: "Document" = Relationship(back_populates="paragraphs") + predictions: list["Prediction"] = Relationship(back_populates="paragraph") + class ParagraphUpdate(BaseModel): text: str | None = None diff --git a/aymurai/database/meta/prediction.py b/aymurai/database/meta/prediction.py index de0f1ef..5565855 100644 --- a/aymurai/database/meta/prediction.py +++ b/aymurai/database/meta/prediction.py @@ -7,9 +7,13 @@ from aymurai.meta.api_interfaces import DocLabel -from aymurai.database.meta.model import Model, ModelPublic +from aymurai.database.meta.model import ModelPublic +from typing import TYPE_CHECKING from aymurai.database.utils import text_to_hash +# if TYPE_CHECKING: +from aymurai.database.schema import Model, Paragraph + class PredictionBase(SQLModel): id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) @@ -21,6 +25,7 @@ class PredictionBase(SQLModel): validation: list[DocLabel] | None = Field(None, sa_column=Column(JSON)) fk_model: uuid.UUID = Field(foreign_key="model.id") + fk_paragraph: uuid.UUID | None = Field(None, foreign_key="paragraph.id") @model_validator(mode="after") def validate_input(self) -> "PredictionBase": @@ -41,6 +46,7 @@ class Prediction(PredictionBase, table=True): ) model: "Model" = Relationship(back_populates="predictions") + paragraph: Paragraph | None = Relationship(back_populates="predictions") class PredictionCreate(PredictionBase): diff --git a/aymurai/database/versions/b09c785624d8_create_database.py b/aymurai/database/versions/6a418ffd84da_create_database.py similarity index 98% rename from aymurai/database/versions/b09c785624d8_create_database.py rename to aymurai/database/versions/6a418ffd84da_create_database.py index d33f06a..7ead196 100644 --- a/aymurai/database/versions/b09c785624d8_create_database.py +++ b/aymurai/database/versions/6a418ffd84da_create_database.py @@ -1,8 +1,8 @@ """Create database -Revision ID: b09c785624d8 +Revision ID: 6a418ffd84da Revises: -Create Date: 2025-04-05 20:28:12.090153 +Create Date: 2025-04-06 02:50:55.031170 """ from typing import Sequence, Union @@ -13,7 +13,7 @@ # revision identifiers, used by Alembic. -revision: str = "b09c785624d8" +revision: str = "6a418ffd84da" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -361,6 +361,7 @@ def upgrade() -> None: sa.Column("prediction", sa.JSON(), nullable=True), sa.Column("validation", sa.JSON(), nullable=True), sa.Column("fk_model", sa.Uuid(), nullable=False), + sa.Column("fk_paragraph", sa.Uuid(), nullable=True), sa.Column("input_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column( "created_at", @@ -373,6 +374,10 @@ def upgrade() -> None: ["fk_model"], ["model.id"], ), + sa.ForeignKeyConstraint( + ["fk_paragraph"], + ["paragraph.id"], + ), sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### From 16f967cdb10722619c749a45a91b855ad80228bf Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Sun, 6 Apr 2025 03:06:53 +0000 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=94=A7=20refactor:=20Update=20Makef?= =?UTF-8?q?ile=20to=20streamline=20build=20and=20database=20migration=20co?= =?UTF-8?q?mmands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9b02cce..d1aef83 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,13 @@ export $(shell sed 's/=.*//' .env.common) api-build: + uv build docker compose build aymurai-api-dev api-run: docker compose run --service-ports aymurai-api-dev api-prod-build: api-build + uv build docker compose build aymurai-api-prod api-prod-run: docker compose run aymurai-api-prod @@ -29,5 +31,5 @@ alembic-regenerate: rm -rvf resources/cache/sqlite/* && \ rm -rvf aymurai/database/versions/* && \ cd aymurai && \ - alembic revision --autogenerate -m "Create database" && \ - alembic upgrade head \ No newline at end of file + uv run alembic revision --autogenerate -m "Create database" && \ + uv run alembic upgrade head \ No newline at end of file From f9a8427d66bd91376489dedc6950c232e041d3cc Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Wed, 16 Apr 2025 16:08:51 +0000 Subject: [PATCH 04/11] feat: enhanced entity resolution and dynamic grouping for anonymization model --- .../05-entity-grouping/entitty-groups.ipynb | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb diff --git a/notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb b/notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb new file mode 100644 index 0000000..3d4a74c --- /dev/null +++ b/notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb @@ -0,0 +1,355 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "\n", + "import requests\n", + "from tqdm import tqdm\n", + "\n", + "API_URL = \"http://localhost:8999\" # Url for debugger. change it to your own" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sample document" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "doc_path = \"/resources/data/sample/document-01.docx\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## /document-extract endpoint output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Function to extract document using the API\n", + "def extract_document(file_path: str) -> dict:\n", + " # Open the file in binary mode and send the POST request\n", + " with open(file_path, \"rb\") as file:\n", + " files = {\"file\": file}\n", + " response = requests.post(url=f\"{API_URL}/document-extract\", files=files)\n", + " response.raise_for_status()\n", + " return response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# /document-extract endpoint output\n", + "extracted_document = extract_document(doc_path)\n", + "extracted_document" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(extracted_document[\"document\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Function to make inference using the API\n", + "def get_predictions(sample: str) -> dict:\n", + " response = requests.post(url=f\"{API_URL}/anonymizer/predict\", json={\"text\": sample})\n", + " response.raise_for_status()\n", + " return response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "predictions = [\n", + " get_predictions(paragraph) for paragraph in tqdm(extracted_document[\"document\"])\n", + "]\n", + "predictions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "predictions[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from itertools import groupby\n", + "\n", + "\n", + "def get_entities(prediction):\n", + " return prediction[\"labels\"]\n", + "\n", + "\n", + "# entities = [entity for prediction in predictions for entity in get_entities(prediction)]\n", + "# entities = sorted(entities, key=lambda x: x[\"attrs\"][\"aymurai_label\"])\n", + "\n", + "entities = [entity for prediction in predictions for entity in get_entities(prediction)]\n", + "entities = [(i, entity) for i, entity in enumerate(entities)]\n", + "entities = sorted(entities, key=lambda x: x[1][\"attrs\"][\"aymurai_label\"])\n", + "\n", + "\n", + "groups = {\n", + " label: list(group)\n", + " for label, group in groupby(entities, key=lambda x: x[1][\"attrs\"][\"aymurai_label\"])\n", + "}\n", + "groups" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import jiwer\n", + "\n", + "group = groups[\"PER\"]\n", + "example = group[0]\n", + "# other = [ent[\"text\"] for ent in group[1:]]\n", + "other = [ent[1][\"text\"] for ent in group[1:]]\n", + "\n", + "display(example)\n", + "\n", + "scores = {\n", + " \"cer\": [jiwer.cer(example[1][\"text\"], text) for text in other],\n", + " \"wer\": [jiwer.wer(example[1][\"text\"], text) for text in other],\n", + " \"mer\": [jiwer.mer(example[1][\"text\"], text) for text in other],\n", + "}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import regex\n", + "import unicodedata\n", + "from jarowinkler import jarowinkler_similarity\n", + "\n", + "\n", + "def normalize_text(text):\n", + " # normalize tildes\n", + " text = unicodedata.normalize(\"NFKD\", text)\n", + " text = \"\".join(char for char in text if unicodedata.category(char) != \"Mn\")\n", + "\n", + " # remove extra spaces and special characters\n", + " text = regex.sub(r\"\\s+\", \" \", text)\n", + " text = regex.sub(r\"\\p{P}\", \"\", text)\n", + "\n", + " # lowercase\n", + " text = text.lower()\n", + "\n", + " return text\n", + "\n", + "\n", + "def compute_norm_cer(x, y):\n", + " x = normalize_text(x)\n", + " y = normalize_text(y)\n", + " return jiwer.cer(x, y) / len(x)\n", + "\n", + "\n", + "def compute_jaro_winkler(x, y):\n", + " x = normalize_text(x)\n", + " y = normalize_text(y)\n", + " return jarowinkler_similarity(x.split(), y.split())\n", + "\n", + "\n", + "def compute_word_subset(x, y):\n", + " x = normalize_text(x)\n", + " y = normalize_text(y)\n", + "\n", + " x = set(x.split())\n", + " y = set(y.split())\n", + " return bool(x & y)\n", + "\n", + "\n", + "scores = {\n", + " \"cer\": np.array(\n", + " [\n", + " [\n", + " compute_norm_cer(sample1[1][\"text\"], sample2[1][\"text\"])\n", + " for sample2 in group\n", + " ]\n", + " for sample1 in group\n", + " ]\n", + " ),\n", + " \"jaro_winkler\": np.array(\n", + " [\n", + " [\n", + " compute_jaro_winkler(sample1[1][\"text\"], sample2[1][\"text\"])\n", + " for sample2 in group\n", + " ]\n", + " for sample1 in group\n", + " ]\n", + " ),\n", + " \"word_subset\": 1\n", + " - np.array(\n", + " [\n", + " [\n", + " compute_word_subset(sample1[1][\"text\"], sample2[1][\"text\"])\n", + " for sample2 in group\n", + " ]\n", + " for sample1 in group\n", + " ]\n", + " ),\n", + "}\n", + "\n", + "scores[\"jaro_winkler\"].shape, scores[\"word_subset\"].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.cluster import DBSCAN\n", + "\n", + "dbscan = DBSCAN(eps=0.01, min_samples=2)\n", + "\n", + "clusters = dbscan.fit_predict(scores[\"word_subset\"])\n", + "clusters\n", + "\n", + "labels = set(clusters)\n", + "labels.discard(-1)\n", + "centroids = [\n", + " \" \".join(\n", + " [normalize_text(group[i][1][\"text\"]) for i in np.where(clusters == label)[0]]\n", + " )\n", + " for label in labels\n", + "]\n", + "centroids\n", + "\n", + "centroids_adj = np.array(\n", + " [[compute_word_subset(c1, c2) for c2 in centroids] for c1 in centroids]\n", + ")\n", + "centroids_adj\n", + "new_labels = np.argmax(centroids_adj, axis=-1)\n", + "\n", + "clusters = np.array([new_labels[label] if label != -1 else -1 for label in clusters])\n", + "clusters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "results = pd.DataFrame(\n", + " {\n", + " \"text\": [group[i][1][\"text\"] for i in range(len(clusters))],\n", + " \"norm_text\": [\n", + " normalize_text(group[i][1][\"text\"]) for i in range(len(clusters))\n", + " ],\n", + " \"index\": [group[i][0] for i in range(len(clusters))],\n", + " \"cluster\": clusters,\n", + " }\n", + ")\n", + "\n", + "results = results.sort_values(\"cluster\")\n", + "\n", + "\n", + "results.groupby(\"cluster\").apply(lambda x: x[\"norm_text\"].tolist()).to_dict()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 8a8192caad18727adb1fd1efe1a0143d0ad763e9 Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Thu, 15 May 2025 05:02:25 +0000 Subject: [PATCH 05/11] feat(database): create initial database schema with multiple tables for documents, paragraphs, and predictions refactor(api): replace DocLabel definition and update DocumentInformation to allow optional labels feat(entities): add DocLabel class for document labeling functionality feat(xml): introduce XML document handling with classes for text fragments and metadata chore(settings): add new settings for Swagger UI and development mode, enhance environment loading with logging feat(anonymization): implement core anonymization logic with alignment and XML handling for DOCX files feat(xml_docx): add functions to unzip, normalize, and replace text in XML documents for anonymization --- aymurai/api/core.py | 50 +-- .../endpoints/routers/anonymizer/__init__.py | 1 + .../routers/anonymizer/anonymizer.py | 360 ---------------- .../api/endpoints/routers/anonymizer/core.py | 67 +++ .../endpoints/routers/anonymizer/document.py | 175 ++++++++ .../endpoints/routers/anonymizer/paragraph.py | 199 +++++++++ .../endpoints/routers/document/__init__.py | 2 + .../endpoints/routers/document/anonymizer.py | 137 ++++++ .../api/endpoints/routers/document/convert.py | 134 ++++++ .../extract.py} | 7 +- aymurai/api/endpoints/routers/misc/convert.py | 208 --------- .../endpoints/routers/paragraph/__init__.py | 2 + .../endpoints/routers/paragraph/predict.py | 198 +++++++++ aymurai/api/main.py | 73 +++- aymurai/database/crud/document-bkp.py | 71 ++++ aymurai/database/crud/model.py | 37 +- aymurai/database/crud/prediction.py | 85 +++- .../database/meta/datapublic/categories.py | 58 --- aymurai/database/meta/extra.py | 9 + aymurai/database/meta/model.py | 29 +- aymurai/database/meta/paragraph.py | 17 +- aymurai/database/meta/prediction.py | 20 +- aymurai/database/schema.py | 57 +-- .../versions/6a418ffd84da_create_database.py | 400 ------------------ ...ase.py => d6c141e03875_create_database.py} | 88 +++- aymurai/meta/api_interfaces.py | 20 +- aymurai/meta/entities.py | 16 + aymurai/meta/xml_document.py | 30 ++ aymurai/settings.py | 13 +- .../misc => text/anonymization}/__init__.py | 0 aymurai/text/anonymization/alignment.py | 390 +++++++++++++++++ aymurai/text/anonymization/core.py | 70 +++ aymurai/text/anonymization/xml_docx.py | 160 +++++++ 33 files changed, 2009 insertions(+), 1174 deletions(-) delete mode 100644 aymurai/api/endpoints/routers/anonymizer/anonymizer.py create mode 100644 aymurai/api/endpoints/routers/anonymizer/core.py create mode 100644 aymurai/api/endpoints/routers/anonymizer/document.py create mode 100644 aymurai/api/endpoints/routers/anonymizer/paragraph.py create mode 100644 aymurai/api/endpoints/routers/document/__init__.py create mode 100644 aymurai/api/endpoints/routers/document/anonymizer.py create mode 100644 aymurai/api/endpoints/routers/document/convert.py rename aymurai/api/endpoints/routers/{misc/document_extract.py => document/extract.py} (93%) delete mode 100644 aymurai/api/endpoints/routers/misc/convert.py create mode 100644 aymurai/api/endpoints/routers/paragraph/__init__.py create mode 100644 aymurai/api/endpoints/routers/paragraph/predict.py create mode 100644 aymurai/database/crud/document-bkp.py delete mode 100644 aymurai/database/meta/datapublic/categories.py create mode 100644 aymurai/database/meta/extra.py delete mode 100644 aymurai/database/versions/6a418ffd84da_create_database.py rename aymurai/database/versions/{13f78d08e925_create_database.py => d6c141e03875_create_database.py} (57%) create mode 100644 aymurai/meta/xml_document.py rename aymurai/{api/endpoints/routers/misc => text/anonymization}/__init__.py (100%) create mode 100644 aymurai/text/anonymization/alignment.py create mode 100644 aymurai/text/anonymization/core.py create mode 100644 aymurai/text/anonymization/xml_docx.py diff --git a/aymurai/api/core.py b/aymurai/api/core.py index c922345..c527709 100644 --- a/aymurai/api/core.py +++ b/aymurai/api/core.py @@ -1,45 +1,21 @@ from fastapi.routing import APIRouter -from .endpoints.routers.anonymizer import anonymizer -from .endpoints.routers.anonymizer import database as anonymizer_database -from .endpoints.routers.datapublic import datapublic - -from .endpoints.routers.misc import convert, document_extract +from .endpoints.routers import document, paragraph from .endpoints.routers.server import stats +# from .endpoints.routers.datapublic import datapublic + + router = APIRouter() # Server -router.include_router( - stats.router, - prefix="/server/stats", - tags=["server"], -) - -# Anonymizer -router.include_router( - anonymizer.router, - prefix="/anonymizer", - tags=["anonymization/model"], -) -# router.include_router( -# anonymizer_database.router, -# prefix="/anonymizer/database", -# tags=["anonymization/database"], -# ) - -# Datapublic -router.include_router( - datapublic.router, - prefix="/datapublic", - tags=["datapublic/model"], -) - - -# Misc -router.include_router(document_extract.router, tags=["document"], deprecated=True) -router.include_router(document_extract.router, prefix="/misc", tags=["document"]) - -# Document conversion -router.include_router(convert.router, tags=["Document conversion"]) +router.include_router(stats.router, prefix="/server/stats", tags=["server"]) + +# Document +router.include_router(document.extract.router, prefix="/document", tags=["document"]) +router.include_router(document.convert.router, prefix="/document", tags=["document"]) +router.include_router(document.anonymizer.router, tags=["document"]) + +# Paragraph +router.include_router(paragraph.predict.router, tags=["paragraph"]) diff --git a/aymurai/api/endpoints/routers/anonymizer/__init__.py b/aymurai/api/endpoints/routers/anonymizer/__init__.py index e69de29..bbc7b81 100644 --- a/aymurai/api/endpoints/routers/anonymizer/__init__.py +++ b/aymurai/api/endpoints/routers/anonymizer/__init__.py @@ -0,0 +1 @@ +from . import paragraph diff --git a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py deleted file mode 100644 index 3b2ebfe..0000000 --- a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py +++ /dev/null @@ -1,360 +0,0 @@ -import os -import subprocess -import tempfile -from functools import cache -from threading import Lock - -import torch -from fastapi import Body, Depends, Query -from fastapi.responses import FileResponse -from fastapi.routing import APIRouter -from pydantic import UUID5 -from sqlmodel import Session -from starlette.background import BackgroundTask - -from aymurai.api.utils import load_pipeline -from aymurai.database.crud.model import register_model -from aymurai.database.crud.prediction import read_prediction, read_validation -from aymurai.database.schema import ( - Document, - Model, - Prediction, - PredictionCreate, -) -from aymurai.database.session import get_session -from aymurai.database.utils import text_to_uuid -from aymurai.logger import get_logger -from aymurai.meta.api_interfaces import ( - DocLabel, - DocumentInformation, - TextRequest, -) -from aymurai.settings import settings -from aymurai.text.anonymization import DocAnonymizer -from aymurai.utils.misc import get_element - -logger = get_logger(__name__) - - -RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads = 100 # FIXME: polemic ? -pipeline_lock = Lock() - - -router = APIRouter() - - -@cache -def get_model(session: Session = Depends(get_session)) -> Model: - """ - Get the model from the database. - - Args: - session (Session): Database session dependency. - - Returns: - Model: The model from the database. - """ - return register_model( - model_name="flair-anonymizer", - app_version=settings.APP_VERSION, - session=session, - ) - - -# MARK: Predict -@router.post("/predict", response_model=DocumentInformation) -async def anonymizer_paragraph_predict( - text_request: TextRequest = Body( - {"text": "Acusado: Ramiro Marrón DNI 34.555.666."} - ), - use_cache: bool = Query( - True, description="Use cache to store or retrive predictions" - ), - session: Session = Depends(get_session), - model: Model = Depends(get_model), -) -> DocumentInformation: - """ - Endpoint to predict anonymization for a given paragraph of text. - - Args: - text_request (TextRequest): The request body containing the text to be anonymized. - use_cache (bool): Flag to determine whether to use cache for storing or retrieving predictions. - session (Session): Database session dependency. - - Returns: - DocumentInformation: The anonymized document information including the text and labels. - """ - - logger.info(f"Running prediction: {model.name} ({model.version})") - - logger.info(f"Checking cache (use cache: {use_cache})") - input_text = text_request.text - prediction_id = text_to_uuid(f"{input_text}-{model.id}") - - ################################################################################## - # Load from cache - ################################################################################## - cached_prediction = read_prediction( - text=input_text, model_id=model.id, session=session - ) - if cached_prediction and use_cache: - logger.info(f"cache loaded from prediction: {cached_prediction.id}") - logger.debug(f"{cached_prediction}") - - return DocumentInformation( - document=cached_prediction.input, labels=cached_prediction.prediction - ) - - ################################################################################## - # Predict - ################################################################################## - - logger.info("Running prediction") - item = [{"path": "empty", "data": {"doc.text": text_request.text}}] - pipeline = load_pipeline( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") - ) - - with pipeline_lock: - processed = pipeline.preprocess(item) - processed = pipeline.predict_single(processed[0]) - processed = pipeline.postprocess([processed]) - - output_text = get_element(processed[0], ["data", "doc.text"]) or "" - output_labels = get_element(processed[0], ["predictions", "entities"]) or [] - - ################################################################################## - # Sanity check - ################################################################################## - if input_text != output_text: - logger.critical( - f"Input and output text do not match: {input_text} != {output_text}" - ) - if settings.ERROR_HANDLER == "raise": - raise ValueError("Input and output text do not match") - - # Run validation on the output labels - output_labels = [ - DocLabel(**label).model_dump(mode="json") for label in output_labels - ] - - ################################################################################# - # Save to cache - ################################################################################# - logger.info(f"saving in cache: {prediction_id}") - pred = session.get(Prediction, prediction_id) - if pred: - logger.warning(f"Prediction already exists: {prediction_id}. Updating.") - pred.input = output_text - pred.prediction = output_labels - else: - pred = PredictionCreate( - input=output_text, - prediction=output_labels, - fk_model=model.id, - ).compile() - - session.add(pred) - session.commit() - session.refresh(pred) - - return DocumentInformation(document=pred.input, labels=pred.prediction) - - -# MARK: Validate -@router.post("/validation", response_model=list[DocLabel] | None) -async def anonymizer_get_paragraph_validation( - text_request: TextRequest = Body( - {"text": "Acusado: Ramiro Marrón DNI 34.555.666."} - ), - session: Session = Depends(get_session), - model: Model = Depends(get_model), -) -> list[DocLabel] | None: - """ - Get the validation labels for a given paragraph text. - - Args: - text_request (TextRequest): The request body containing the text to be validated. - session (Session): Database session dependency. - - Returns: - list[DocLabel] | None: A list of validation labels for the given paragraph text, or None if no validation exists. - """ - - text = text_request.text - - logger.info(f"Checking validation: {model.name} ({model.version})") - pred = read_validation(input_hash=text, model_name=model.name, session=session) - - if not pred: - return None - - return pred.validation - - -# MARK: Document Compilation -@router.post("/anonymize-document") -async def anonymizer_compile_document( - document_id: UUID5, - annotations: list[DocumentInformation], - session: Session = Depends(get_session), - model: Model = Depends(get_model), -) -> FileResponse: - """ - Compile Anonimized document from original file and annotations - - Args: - file (UploadFile): Original file. - annotations (str, optional): JSON with document annotations. - - Returns: - FileResponse: Anonymized document - """ - - document = session.get(Document, document_id) - - ################################################################################## - # Sanity check - ################################################################################## - # Check if the document exists - if not document: - raise ValueError(f"Document not found: {document_id}") - - # Check length of annotations and document match - if len(annotations) != len(document.paragraphs): - raise ValueError( - f"The number of annotated paragraphs ({len(annotations)}) does not" - f" match the number of paragraphs in the document" - f" ({len(document.paragraphs)})" - ) - - # Check if the annotated paragraphs match the document paragraphs one by one - for i, (annot_paragraph, doc_paragraph) in enumerate( - zip(annotations, document.paragraphs) - ): - if annot_paragraph.document != doc_paragraph.text: - msg = ( - f"The annotated paragraph ({annot_paragraph.document}) does not match" - f" the document paragraph ({doc_paragraph.text})" - ) - if settings.ERROR_HANDLER == "raise": - raise ValueError(msg) - logger.warning(msg) - - #################################################################################### - # Updating validations - # #################################################################################### - - predictions = [] - for paragraph in annotations: - prediction = read_prediction( - text=paragraph.document, model_id=model.id, session=session - ) - if prediction: - prediction.validation = paragraph.labels - predictions.append(prediction) - else: - logger.warning("Prediction not found.") - if settings.ERROR_HANDLER == "raise": - raise ValueError( - f"Prediction not found for paragraph: `{paragraph.document}`" - ) - prediction = Prediction( - input=paragraph.document, - fk_model=model.id, - validation=paragraph.labels, - ) - predictions.append(prediction) - - session.add_all(predictions) - session.commit() - for prediction in predictions: - session.refresh(prediction) - - #################################################################################### - # Create a temporary file - #################################################################################### - - filename = document.name - logger.info(f"Document found: {document_id} ({filename})") - - # Create a temporary file - _, suffix = os.path.splitext(filename) - suffix = suffix if suffix == ".docx" else ".txt" - tmp_dir = tempfile.gettempdir() - - # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed - # This is necessary on Windows, as the file is locked by the file object and cannot be deleted - with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, dir=tmp_dir) as file: - tmp_filename = file.name - logger.info(f"saving temp file on local storage => {tmp_filename}") - file.write(document.data) - file.flush() - file.close() - - logger.info(f"saved temp file on local storage => {tmp_filename}") - - #################################################################################### - # Anonymize the document - #################################################################################### - doc_anonymizer = DocAnonymizer() - - if suffix == ".docx": - item = {"path": tmp_filename} - doc_anonymizer( - item, - [document_information.model_dump() for document_information in annotations], - tmp_dir, - ) - logger.info(f"saved temp file on local storage => {tmp_filename}") - - else: - # Export as raw document - anonymized_doc = [ - doc_anonymizer.replace_labels_in_text(document_information.model_dump()) - .replace("<", "<") - .replace(">", ">") - for document_information in annotations - ] - with open(tmp_filename, "w") as f: - f.write("\n".join(anonymized_doc)) - - # Convert to ODT - cmd = [ - settings.LIBREOFFICE_BIN, - "--headless", - "--convert-to", - "odt", - "--outdir", - tmp_dir, - tmp_filename, - ] - - logger.info(f"Executing: {' '.join(cmd)}") - - try: - output = subprocess.check_output( - cmd, shell=False, encoding="utf-8", errors="ignore" - ) - logger.info(f"LibreOffice output: {output}") - except subprocess.CalledProcessError as e: - raise RuntimeError( - f"LibreOffice conversion failed: {e.output.decode('utf-8', errors='ignore')}" - ) - - odt = tmp_filename.replace(suffix, ".odt") - logger.info(f"Expected output file path: {odt}") - - if not os.path.exists(odt): - raise RuntimeError(f"File at path {odt} does not exist.") - - # Ensure the temporary file is deleted - os.remove(tmp_filename) - - return FileResponse( - odt, - background=BackgroundTask(os.remove, odt), - media_type="application/octet-stream", - filename=f"{os.path.splitext(filename)[0]}.odt", - ) diff --git a/aymurai/api/endpoints/routers/anonymizer/core.py b/aymurai/api/endpoints/routers/anonymizer/core.py new file mode 100644 index 0000000..8d23d65 --- /dev/null +++ b/aymurai/api/endpoints/routers/anonymizer/core.py @@ -0,0 +1,67 @@ +import os +from functools import cache + +from fastapi import Depends +from sqlmodel import Session + +from aymurai.api.utils import load_pipeline +from aymurai.database.crud.model import register_model +from aymurai.database.schema import Model +from aymurai.database.session import get_session +from aymurai.logger import get_logger +from aymurai.pipeline.pipeline import AymurAIPipeline +from aymurai.settings import settings + +logger = get_logger(__name__) + +RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH + + +@cache +def get_anonimization_model(session: Session = Depends(get_session)) -> Model: + """ + Get the model from the database. + + Args: + session (Session): Database session dependency. + + Returns: + Model: The model from the database. + """ + return register_model( + model_name="flair-anonymizer", + app_version=settings.APP_VERSION, + session=session, + ) + + +@cache +def get_datapublic_model(session: Session = Depends(get_session)) -> Model: + """ + Get the model from the database. + + Args: + session (Session): Database session dependency. + + Returns: + Model: The model from the database. + """ + return register_model( + model_name="datapublic", + app_version=settings.APP_VERSION, + session=session, + ) + + +@cache +def load_anonymization_pipeline() -> AymurAIPipeline: + return load_pipeline( + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") + ) + + +@cache +def load_datapublic_pipeline() -> AymurAIPipeline: + return load_pipeline( + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "full-paragraph") + ) diff --git a/aymurai/api/endpoints/routers/anonymizer/document.py b/aymurai/api/endpoints/routers/anonymizer/document.py new file mode 100644 index 0000000..434dbfe --- /dev/null +++ b/aymurai/api/endpoints/routers/anonymizer/document.py @@ -0,0 +1,175 @@ +import os +import subprocess +import tempfile +from functools import cache +from threading import Lock + +import torch +from fastapi import Body, Depends, Query, HTTPException +from fastapi.responses import FileResponse +from fastapi.routing import APIRouter +from pydantic import UUID4 +from sqlmodel import Session +from starlette.background import BackgroundTask + +from aymurai.api.utils import load_pipeline +from aymurai.database.crud.model import register_model +from aymurai.database.crud.prediction import read_prediction_by_text, read_validation +from aymurai.database.schema import ( + Document, + Model, + Prediction, + PredictionCreate, + Paragraph, +) +from aymurai.database.session import get_session +from aymurai.database.utils import text_to_uuid +from aymurai.logger import get_logger +from aymurai.meta.api_interfaces import ( + DocLabel, + DocumentInformation, + TextRequest, +) +from aymurai.settings import settings +from aymurai.text.anonymization import DocAnonymizer +from aymurai.utils.misc import get_element + +logger = get_logger(__name__) + + +RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH +torch.set_num_threads = 100 # FIXME: polemic ? +pipeline_lock = Lock() + + +router = APIRouter() + + +@cache +def get_model(session: Session = Depends(get_session)) -> Model: + """ + Get the model from the database. + + Args: + session (Session): Database session dependency. + + Returns: + Model: The model from the database. + """ + return register_model( + model_name="flair-anonymizer", + app_version=settings.APP_VERSION, + session=session, + ) + + +# MARK: Document Compilation +@router.post("/document/{document_id}/compile", response_model=FileResponse) +async def anonymizer_compile_document( + document_id: UUID4, + session: Session = Depends(get_session), + model: Model = Depends(get_model), +) -> FileResponse: + """ + Compile Anonymized document from original file and annotations + + Args: + file (UploadFile): Original file. + annotations (str, optional): JSON with document annotations. + + Returns: + FileResponse: Anonymized document + """ + document = session.get(Document, document_id) + + # ———————— Sanity check —————————————————————————————————————————————————————————— + # Check if the document exists + if not document: + raise ValueError(f"Document not found: {document_id}") + + #################################################################################### + # Create a temporary file + #################################################################################### + + filename = document.name + logger.info(f"Document found: {document_id} ({filename})") + + # Create a temporary file + _, suffix = os.path.splitext(filename) + suffix = suffix if suffix == ".docx" else ".txt" + tmp_dir = tempfile.gettempdir() + + # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed + # This is necessary on Windows, as the file is locked by the file object and cannot be deleted + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, dir=tmp_dir) as file: + tmp_filename = file.name + logger.info(f"saving temp file on local storage => {tmp_filename}") + file.write(document.data) + file.flush() + file.close() + + logger.info(f"saved temp file on local storage => {tmp_filename}") + + #################################################################################### + # Anonymize the document + #################################################################################### + doc_anonymizer = DocAnonymizer() + + if suffix == ".docx": + item = {"path": tmp_filename} + doc_anonymizer( + item, + [document_information.model_dump() for document_information in annotations], + tmp_dir, + ) + logger.info(f"saved temp file on local storage => {tmp_filename}") + + else: + # Export as raw document + anonymized_doc = [ + doc_anonymizer.replace_labels_in_text(document_information.model_dump()) + .replace("<", "<") + .replace(">", ">") + for document_information in annotations + ] + with open(tmp_filename, "w") as f: + f.write("\n".join(anonymized_doc)) + + # Convert to ODT + cmd = [ + settings.LIBREOFFICE_BIN, + "--headless", + "--convert-to", + "odt", + "--outdir", + tmp_dir, + tmp_filename, + ] + + logger.info(f"Executing: {' '.join(cmd)}") + + try: + output = subprocess.check_output( + cmd, shell=False, encoding="utf-8", errors="ignore" + ) + logger.info(f"LibreOffice output: {output}") + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"LibreOffice conversion failed: {e.output.decode('utf-8', errors='ignore')}" + ) + + odt = tmp_filename.replace(suffix, ".odt") + logger.info(f"Expected output file path: {odt}") + + if not os.path.exists(odt): + raise RuntimeError(f"File at path {odt} does not exist.") + + # Ensure the temporary file is deleted + os.remove(tmp_filename) + + return FileResponse( + odt, + background=BackgroundTask(os.remove, odt), + media_type="application/octet-stream", + filename=f"{os.path.splitext(filename)[0]}.odt", + ) diff --git a/aymurai/api/endpoints/routers/anonymizer/paragraph.py b/aymurai/api/endpoints/routers/anonymizer/paragraph.py new file mode 100644 index 0000000..de827c6 --- /dev/null +++ b/aymurai/api/endpoints/routers/anonymizer/paragraph.py @@ -0,0 +1,199 @@ +import os +from threading import Lock + +import torch +from fastapi import Depends, Query, HTTPException +from fastapi.routing import APIRouter +from pydantic import UUID4 +from sqlmodel import Session + +from aymurai.api.utils import load_pipeline +from aymurai.api.endpoints.routers.anonymizer.core import get_anonimization_model +from aymurai.database.schema import ( + Document, + Model, + Prediction, + PredictionCreate, + Paragraph, +) +from aymurai.database.crud.prediction import read_prediction_by_text +from aymurai.database.session import get_session +from aymurai.logger import get_logger +from aymurai.meta.api_interfaces import ( + DocLabel, + DocumentInformation, +) +from aymurai.settings import settings +from aymurai.utils.misc import get_element + +logger = get_logger(__name__) + + +RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH +torch.set_num_threads(100) # FIXME: polemic ? +pipeline_lock = Lock() + + +router = APIRouter() + + +# MARK: Paragraph Predict +@router.post("/paragraph/{paragraph_id}", response_model=DocumentInformation) +async def anonymizer_paragraph_predict( + paragraph_id: UUID4, + use_cache: bool = Query( + True, description="Use cache to store or retrive predictions" + ), + session: Session = Depends(get_session), + model: Model = Depends(get_anonimization_model), +) -> DocumentInformation: + """ + Endpoint to predict anonymization for a given paragraph of text. + + Args: + text_request (TextRequest): The request body containing the text to be anonymized. + use_cache (bool): Flag to determine whether to use cache for storing or retrieving predictions. + session (Session): Database session dependency. + + Returns: + DocumentInformation: The anonymized document information including the text and labels. + """ + + paragraph = session.get(Paragraph, paragraph_id) + if not paragraph: + raise HTTPException( + status_code=404, + detail=f"Paragraph not found: {paragraph_id}", + ) + input_text = paragraph.text + + logger.info(f"Running prediction: {model.name} ({model.version})") + + logger.info(f"Checking cache (use cache: {use_cache})") + + # —————— Check cache ————————————————————————————————————————————————————————————— + cached_pred = read_prediction_by_text( + text=input_text, model_id=model.id, session=session + ) + if cached_pred and use_cache: + logger.info(f"cache loaded from prediction: {cached_pred.id}") + logger.debug(f"{cached_pred}") + + labels = cached_pred.validation or cached_pred.prediction + + return DocumentInformation(document=cached_pred.input, labels=labels) + + # ——————— Run prediction ————————————————————————————————————————————————————————— + logger.info("Running prediction") + item = [{"path": "empty", "data": {"doc.text": input_text}}] + pipeline = load_pipeline( + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") + ) + + with pipeline_lock: + processed = pipeline.preprocess(item) + processed = pipeline.predict_single(processed[0]) + processed = pipeline.postprocess([processed]) + + output_text = get_element(processed[0], ["data", "doc.text"]) or "" + output_labels = get_element(processed[0], ["predictions", "entities"]) or [] + + # ———————— Sanity check —————————————————————————————————————————————————————————— + if input_text != output_text: + logger.critical( + f"Input and output text do not match: {input_text} != {output_text}" + ) + if settings.ERROR_HANDLER == "raise": + raise ValueError("Input and output text do not match") + + # Run validation on the output labels + output_labels = [ + DocLabel(**label).model_dump(mode="json") for label in output_labels + ] + + # ——————— Save to cache —————————————————————————————————————————————————————————— + if cached_pred: + logger.info(f"saving in cache: {cached_pred.id}") + + logger.warning(f"Prediction already exists: {cached_pred}. Updating.") + cached_pred.input = output_text + cached_pred.prediction = output_labels + else: + pred = PredictionCreate( + input=output_text, + prediction=output_labels, + fk_model=model.id, + fk_paragraph=paragraph.id, + ).compile() + + session.add(cached_pred if cached_pred else pred) + session.commit() + session.refresh(pred) + + return DocumentInformation(document=pred.input, labels=pred.prediction) + + +# MARK: Paragraph Validate +@router.post("/paragraph/{paragraph_id}/validate") +async def anonymizer_save_document_validation( + paragraph_id: UUID4, + annotations: list[DocumentInformation], + session: Session = Depends(get_session), + model: Model = Depends(get_anonimization_model), +) -> list[DocLabel] | None: + document = session.get(Document, paragraph_id) + + # ———————— Sanity check —————————————————————————————————————————————————————————— + # Check if the document exists + if not document: + raise ValueError(f"Document not found: {paragraph_id}") + + # Check length of annotations and document match + if len(annotations) != len(document.paragraphs): + raise ValueError( + f"The number of annotated paragraphs ({len(annotations)}) does not" + f" match the number of paragraphs in the document" + f" ({len(document.paragraphs)})" + ) + + # Check if the annotated paragraphs match the document paragraphs one by one + for i, (annot_paragraph, doc_paragraph) in enumerate( + zip(annotations, document.paragraphs) + ): + if annot_paragraph.document != doc_paragraph.text: + msg = ( + f"The annotated paragraph ({annot_paragraph.document}) does not match" + f" the document paragraph ({doc_paragraph.text})" + ) + if settings.ERROR_HANDLER == "raise": + raise ValueError(msg) + logger.warning(msg) + + # ——————— Updating validations ——————————————————————————————————————————————————— + predictions = [] + for paragraph in annotations: + prediction = read_prediction_by_text( + text=paragraph.document, model_id=model.id, session=session + ) + if prediction: + prediction.validation = paragraph.labels + predictions.append(prediction) + else: + logger.warning("Prediction not found.") + if settings.ERROR_HANDLER == "raise": + raise ValueError( + f"Prediction not found for paragraph: `{paragraph.document}`" + ) + prediction = Prediction( + input=paragraph.document, + fk_model=model.id, + validation=paragraph.labels, + ) + predictions.append(prediction) + + session.add_all(predictions) + session.commit() + for prediction in predictions: + session.refresh(prediction) + + return diff --git a/aymurai/api/endpoints/routers/document/__init__.py b/aymurai/api/endpoints/routers/document/__init__.py new file mode 100644 index 0000000..979d6fc --- /dev/null +++ b/aymurai/api/endpoints/routers/document/__init__.py @@ -0,0 +1,2 @@ +# ruff: noqa: F401 +from . import anonymizer, convert, extract diff --git a/aymurai/api/endpoints/routers/document/anonymizer.py b/aymurai/api/endpoints/routers/document/anonymizer.py new file mode 100644 index 0000000..0387f85 --- /dev/null +++ b/aymurai/api/endpoints/routers/document/anonymizer.py @@ -0,0 +1,137 @@ +import os +import subprocess +import tempfile + +from fastapi import Depends +from fastapi.responses import FileResponse +from fastapi.routing import APIRouter +from pydantic import UUID4, UUID5 +from sqlmodel import Session +from starlette.background import BackgroundTask + +from aymurai.database.crud.prediction import read_document_prediction_paragraphs +from aymurai.database.schema import Document, ModelType +from aymurai.database.session import get_session +from aymurai.logger import get_logger +from aymurai.settings import settings +from aymurai.text.anonymization.alignment import replace_labels_in_text +from aymurai.text.anonymization.core import anonymize_docx + +logger = get_logger(__name__) + + +router = APIRouter() + + +# MARK: Document Compilation +@router.get( + f"/document/{{document_id}}/pipeline/{ModelType.ANONYMIZATION}/compile", + response_class=FileResponse, +) +async def anonymizer_compile_document( + document_id: UUID4 | UUID5, + session: Session = Depends(get_session), +) -> FileResponse: + """ + Compile Anonymized document from original file and annotations + + Args: + document_id (UUID4): The ID of the document to be compiled. + session (Session): Database session dependency. + + Returns: + FileResponse: Anonymized document + """ + document = session.get(Document, document_id) + + # ———————— Sanity check ——————————————————————————————————————————————————————————— + # Check if the document exists + if not document: + raise ValueError(f"Document not found: {document_id}") + + # ———————— Get annotations ———————————————————————————————————————————————————————— + annotations = read_document_prediction_paragraphs( + session=session, + document_id=document_id, + model_type=ModelType.ANONYMIZATION, + ) + + # ======== Document Anonymization ================================================= + # ———————— Create temporary file —————————————————————————————————————————————————— + filename = document.name + logger.info(f"Document found: {document_id} ({filename})") + + # Create a temporary file + _, suffix = os.path.splitext(filename) + suffix = suffix.lower() + suffix = suffix if suffix == ".docx" else ".txt" + tmp_dir = tempfile.gettempdir() + + # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed + # This is necessary on Windows, as the file is locked by the file object and cannot be deleted + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, dir=tmp_dir) as file: + tmp_filename = file.name + logger.info(f"saving temp file on local storage => {tmp_filename}") + file.write(document.data) + file.flush() + file.close() + + logger.info(f"saved temp file on local storage => {tmp_filename}") + + # ———————— Anonymize document ————————————————————————————————————————————————————— + + if suffix == ".docx": + anonymize_docx( + path=tmp_filename, paragraph_preds=annotations, output_dir=tmp_dir + ) + logger.info(f"saved temp file on local storage => {tmp_filename}") + + else: + # Export as raw document + anonymized_doc = [ + replace_labels_in_text(text=paragraph.text, prediction=paragraph.prediction) + .replace("<", "<") + .replace(">", ">") + for paragraph in annotations + ] + with open(tmp_filename, "w") as f: + f.write("\n".join(anonymized_doc)) + + # ——————— Convert to ODT —————————————————————————————————————————————————————————— + cmd = [ + settings.LIBREOFFICE_BIN, + "--headless", + "--convert-to", + "odt", + "--outdir", + tmp_dir, + tmp_filename, + ] + + logger.info(f"Executing: {' '.join(cmd)}") + + try: + output = subprocess.check_output( + cmd, shell=False, encoding="utf-8", errors="ignore" + ) + logger.info(f"LibreOffice output: {output}") + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"LibreOffice conversion failed: {e.output.decode('utf-8', errors='ignore')}" + ) + + odt = tmp_filename.replace(suffix, ".odt") + logger.info(f"Expected output file path: {odt}") + + if not os.path.exists(odt): + raise RuntimeError(f"File at path {odt} does not exist.") + + # Ensure the temporary file is deleted + os.remove(tmp_filename) + + return FileResponse( + odt, + background=BackgroundTask(os.remove, odt), + media_type="application/octet-stream", + filename=f"{os.path.splitext(filename)[0]}.odt", + ) diff --git a/aymurai/api/endpoints/routers/document/convert.py b/aymurai/api/endpoints/routers/document/convert.py new file mode 100644 index 0000000..8a67b7e --- /dev/null +++ b/aymurai/api/endpoints/routers/document/convert.py @@ -0,0 +1,134 @@ +import os +import subprocess +import tempfile +from enum import Enum +from typing import Literal + +import pymupdf4llm +import pypandoc +from fastapi import Path, Query, UploadFile +from fastapi.responses import FileResponse +from fastapi.routing import APIRouter +from starlette.background import BackgroundTask + +from aymurai.api.exceptions import UnsupportedFileType +from aymurai.logger import get_logger +from aymurai.settings import settings + +logger = get_logger(__name__) +router = APIRouter() + + +class FileFormat(str, Enum): + pdf = "pdf" + docx = "docx" + odt = "odt" + + +def libreoffice_convert( + input: str, + format: Literal["pdf", "docx", "odt", "txt"], + output_dir=tempfile.gettempdir(), + extra_args: str = "", +) -> str: + cmd = f"{settings.LIBREOFFICE_BIN} --headless --convert-to {format} --outdir {output_dir} {input} {extra_args}" + subprocess.run(cmd, shell=True, check=True) + + filename = os.path.basename(input) + output = os.path.join(output_dir, filename.replace(input.split(".")[-1], format)) + + if not os.path.exists(output): + raise RuntimeError(f"LibreOffice conversion failed: {output}") + + return output + + +async def convert_libreoffice( + file: UploadFile, + output_format: Literal["pdf", "docx", "odt"], + extra_args: str = "", +) -> FileResponse: + _, suffix = os.path.splitext(file.filename) + + tmp_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) + with open(tmp_file.name, "wb") as tmp: + data = file.file.read() + tmp.write(data) + + output = libreoffice_convert( + tmp_file.name, + format=output_format, + extra_args=extra_args, + ) + os.remove(tmp_file.name) + + return FileResponse( + output, + background=BackgroundTask(os.remove, output), + media_type="application/octet-stream", + filename=os.path.basename(file.filename).replace(suffix, f".{output_format}"), + ) + + +async def convert_pdf_pandoc( + file: UploadFile, + output_format: Literal["docx", "odt"], +) -> FileResponse: + _, suffix = os.path.splitext(file.filename) + if suffix != ".pdf": + raise UnsupportedFileType(detail="Expected a .pdf file") + + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=True) as tmp_file: + tmp_file.write(file.file.read()) + tmp_file.flush() + + text = pymupdf4llm.to_markdown( + tmp_file.name, + write_images=True, + embed_images=True, + image_size_limit=0, + ) + + output = tempfile.mktemp(suffix=f".{output_format}") + pypandoc.convert_text(text, output_format, format="md", outputfile=output) + + return FileResponse( + output, + background=BackgroundTask(os.remove, output), + media_type="application/octet-stream", + filename=os.path.basename(file.filename).replace(".pdf", f".{output_format}"), + ) + + +@router.post("/convert/{source}/{target}") +async def convert_file( + file: UploadFile, + source: FileFormat = Path(...), + target: FileFormat = Path(...), + backend: Literal["libreoffice", "pandoc"] = Query("libreoffice"), +) -> FileResponse: + _, suffix = os.path.splitext(file.filename) + if suffix.lower() != f".{source.value}": + raise UnsupportedFileType(detail=f"Expected a .{source.value} file") + if source == target: + raise UnsupportedFileType(detail="Source and target formats are the same.") + + # Set extra_args for libreoffice conversions + extra_args = '--infilter="writer_pdf_import"' if source == FileFormat.pdf else "" + + match (source, backend): + case (FileFormat.pdf, "pandoc"): + return await convert_pdf_pandoc(file, output_format=target.value) + case (_, "libreoffice"): + return await convert_libreoffice( + file, + output_format=target.value, + extra_args=extra_args, + ) + case _: + raise UnsupportedFileType( + detail=( + f"Conversion from {source.value} to {target.value} " + f"with backend {backend} is not supported." + ) + ) diff --git a/aymurai/api/endpoints/routers/misc/document_extract.py b/aymurai/api/endpoints/routers/document/extract.py similarity index 93% rename from aymurai/api/endpoints/routers/misc/document_extract.py rename to aymurai/api/endpoints/routers/document/extract.py index 43b5cc5..51d1e8e 100644 --- a/aymurai/api/endpoints/routers/misc/document_extract.py +++ b/aymurai/api/endpoints/routers/document/extract.py @@ -9,11 +9,10 @@ from sqlmodel import Session from aymurai.api.utils import get_pipeline_doc_extract -from aymurai.database.schema import Paragraph, Document, DocumentPublic +from aymurai.database.schema import Document, DocumentPublic, Paragraph from aymurai.database.session import get_session from aymurai.database.utils import data_to_uuid from aymurai.logger import get_logger - from aymurai.pipeline import AymurAIPipeline from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER from aymurai.utils.misc import get_element @@ -24,7 +23,7 @@ router = APIRouter() -@router.post("/document-extract", response_model=DocumentPublic) +@router.post("/extract", response_model=DocumentPublic) def plain_text_extractor( file: UploadFile, pipeline: AymurAIPipeline = Depends(get_pipeline_doc_extract), @@ -78,7 +77,7 @@ def plain_text_extractor( # Add paragraphs to the database # validation MUST be at least an empty list, to remember user feedback paragraphs = [ - Paragraph(text=paragraph, order=i) for i, paragraph in enumerate(paragraph_text) + Paragraph(text=paragraph, index=i) for i, paragraph in enumerate(paragraph_text) ] paragraph_text = Document( diff --git a/aymurai/api/endpoints/routers/misc/convert.py b/aymurai/api/endpoints/routers/misc/convert.py deleted file mode 100644 index 058534e..0000000 --- a/aymurai/api/endpoints/routers/misc/convert.py +++ /dev/null @@ -1,208 +0,0 @@ -import os -import tempfile -import subprocess -from threading import Lock -from typing import Literal - -import pypandoc -import pymupdf4llm -from fastapi import UploadFile -from fastapi.responses import FileResponse -from fastapi.routing import APIRouter -from starlette.background import BackgroundTask - -from aymurai.api.exceptions import UnsupportedFileType -from aymurai.logger import get_logger -from aymurai.settings import settings - -logger = get_logger(__name__) -pipeline_lock = Lock() - -router = APIRouter() - - -def libreoffice_convert( - input: str, - format: Literal["pdf", "docx", "odt", "txt"], - output_dir=tempfile.gettempdir(), - extra_args: str = "", -) -> str: - cmd = f"{settings.LIBREOFFICE_BIN} --headless --convert-to {format} --outdir {output_dir} {input} {extra_args}" - subprocess.run(cmd, shell=True, check=True) - - filename = os.path.basename(input) - output = os.path.join(output_dir, filename.replace(input.split(".")[-1], format)) - - if not os.path.exists(output): - raise RuntimeError(f"LibreOffice conversion failed: {output}") - - return output - - -async def convert_libreoffice( - file: UploadFile, - output_format: Literal["pdf", "docx", "odt"], - extra_args: str = "", -) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - - tmp_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) - with open(tmp_file.name, "wb") as tmp: - data = file.file.read() - tmp.write(data) - - output = libreoffice_convert( - tmp_file.name, - format=output_format, - extra_args=extra_args, - ) - os.remove(tmp_file.name) - - return FileResponse( - output, - background=BackgroundTask(os.remove, output), - media_type="application/octet-stream", - filename=os.path.basename(file.filename).replace(suffix, f".{output_format}"), - ) - - -async def convert_pdf_pandoc( - file: UploadFile, - output_format: Literal["docx", "odt"], -) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - if suffix != ".pdf": - raise UnsupportedFileType(detail="Expected a .pdf file") - - with tempfile.NamedTemporaryFile(suffix=".pdf", delete=True) as tmp_file: - tmp_file.write(file.file.read()) - tmp_file.flush() - - text = pymupdf4llm.to_markdown( - tmp_file.name, - write_images=True, - embed_images=True, - image_size_limit=0, - ) - - output = tempfile.mktemp(suffix=f".{output_format}") - pypandoc.convert_text(text, output_format, format="md", outputfile=output) - - return FileResponse( - output, - background=BackgroundTask(os.remove, output), - media_type="application/octet-stream", - filename=os.path.basename(file.filename).replace(".pdf", f".{output_format}"), - ) - - -# MARK: PDF -> ODT -@router.post("/convert/pdf/odt") -async def convert_pdf_odt( - file: UploadFile, - backend: Literal["libreoffice", "pandoc"] = "libreoffice", -) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - if suffix != ".pdf": - raise UnsupportedFileType(detail="Expected a .pdf file") - - if backend == "libreoffice": - return await convert_libreoffice( - file, - output_format="odt", - extra_args='--infilter="writer_pdf_import"', - ) - elif backend == "pandoc": - return await convert_pdf_pandoc(file, output_format="odt") - raise ValueError("Invalid backend") - - -# MARK: PDF -> DOCX -@router.post("/convert/pdf/docx") -async def convert_pdf_docx( - file: UploadFile, - backend: Literal["libreoffice", "pandoc"] = "libreoffice", -) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - if suffix != ".pdf": - raise UnsupportedFileType(detail="Expected a .pdf file") - - if backend == "libreoffice": - return await convert_libreoffice( - file, - output_format="docx", - extra_args='--infilter="writer_pdf_import"', - ) - elif backend == "pandoc": - return await convert_pdf_pandoc(file, output_format="docx") - raise ValueError("Invalid backend") - - -# MARK: DOCX -> ODT -@router.post("/convert/docx/odt") -async def convert_docx_odt(file: UploadFile) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - if suffix != ".docx": - raise UnsupportedFileType(detail="Expected a .docx file") - - return await convert_libreoffice(file, output_format="odt") - - -# MARK: DOCX -> PDF -@router.post("/convert/docx/pdf") -async def convert_docx_pdf(file: UploadFile) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - if suffix != ".docx": - raise UnsupportedFileType(detail="Expected a .docx file") - - return await convert_libreoffice(file, output_format="pdf") - - -# MARK: ODT -> PDF -@router.post("/convert/odt/pdf") -async def convert_odt_pdf(file: UploadFile) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - if suffix != ".odt": - raise UnsupportedFileType(detail="Expected a .odt file") - - return await convert_libreoffice(file, output_format="pdf") - - -# MARK: ODT -> DOCX -@router.post("/convert/odt/docx") -async def convert_odt_docx(file: UploadFile) -> FileResponse: - _, suffix = os.path.splitext(file.filename) - if suffix != ".odt": - raise UnsupportedFileType(detail="Expected a .odt file") - - return await convert_libreoffice(file, output_format="docx") - - -# # MARK: TXT -> DOCX -# @router.post("/convert/txt/docx") -# async def convert_txt_docx(file: UploadFile) -> FileResponse: -# _, suffix = os.path.splitext(file.filename) -# if suffix != ".txt": -# raise UnsupportedFileType(detail="Expected a .txt file") - -# return await convert_common_txt_docx_doc(file, output_format="docx") - - -# # MARK: TXT -> ODT -# @router.post("/convert/txt/odt") -# async def convert_txt_odt(file: UploadFile) -> FileResponse: -# _, suffix = os.path.splitext(file.filename) -# if suffix != ".txt": -# raise UnsupportedFileType(detail="Expected a .txt file") - -# return await convert_common_txt_docx_doc(file, output_format="odt") - - -# # MARK: TXT -> PDF -# @router.post("/convert/txt/pdf") -# async def convert_txt_pdf(file: UploadFile) -> FileResponse: -# _, suffix = os.path.splitext(file.filename) -# if suffix != ".txt": -# raise UnsupportedFileType(detail="Expected a .txt file") - -# return await convert_common_txt_docx_doc(file, output_format="odt") diff --git a/aymurai/api/endpoints/routers/paragraph/__init__.py b/aymurai/api/endpoints/routers/paragraph/__init__.py new file mode 100644 index 0000000..43926e1 --- /dev/null +++ b/aymurai/api/endpoints/routers/paragraph/__init__.py @@ -0,0 +1,2 @@ +# ruff: noqa: F401 +from . import predict diff --git a/aymurai/api/endpoints/routers/paragraph/predict.py b/aymurai/api/endpoints/routers/paragraph/predict.py new file mode 100644 index 0000000..14da0d2 --- /dev/null +++ b/aymurai/api/endpoints/routers/paragraph/predict.py @@ -0,0 +1,198 @@ +from threading import Lock + +import torch +from fastapi import Depends, HTTPException, Query +from fastapi.routing import APIRouter +from pydantic import UUID4 +from sqlmodel import Session + +from aymurai.api.utils import load_pipeline +from aymurai.database.crud.model import read_latest_model_by_type +from aymurai.database.crud.prediction import read_prediction_by_text +from aymurai.database.meta.extra import ParagraphPredictionPublic +from aymurai.database.schema import ( + ModelType, + Paragraph, + Prediction, + PredictionCreate, + PredictionPublic, +) +from aymurai.database.session import get_session +from aymurai.logger import get_logger +from aymurai.meta.entities import DocLabel +from aymurai.settings import settings +from aymurai.utils.misc import get_element + +logger = get_logger(__name__) + + +RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH +torch.set_num_threads(100) # FIXME: polemic ? +pipeline_lock = Lock() + + +router = APIRouter() + + +# MARK: Paragraph Predict +@router.get( + "/paragraph/{paragraph_id}/pipeline/{pipeline_type}/predict", + response_model=ParagraphPredictionPublic, +) +async def paragraph_predict( + paragraph_id: UUID4, + pipeline_type: ModelType, + use_cache: bool = Query( + True, description="Use cache to store or retrieve predictions" + ), + session: Session = Depends(get_session), +) -> ParagraphPredictionPublic: + """ + Endpoint to predict for a given paragraph of text. + + Args: + paragraph_id (UUID4): The ID of the paragraph to be predicted. + pipeline_type (ModelType): The type of model to be used for prediction. + use_cache (bool): Flag to determine whether to use cache for storing or retrieving predictions. + session (Session): Database session dependency. + + Returns: + ParagraphPredictionPublic: The predicted document information including the text and labels. + """ + model = read_latest_model_by_type(model_type=pipeline_type, session=session) + pipeline = load_pipeline(model.pipeline_path) + + paragraph = session.get(Paragraph, paragraph_id) + + # ———————— Sanity check —————————————————————————————————————————————————————————— + if not paragraph: + raise HTTPException( + status_code=404, + detail=f"Paragraph not found: {paragraph_id}", + ) + + logger.info(f"Running prediction: {model.name} ({model.version})") + + input_text = paragraph.text + # —————— Check cache ————————————————————————————————————————————————————————————— + logger.info(f"Checking cache (use cache: {use_cache})") + cached_pred = read_prediction_by_text( + text=input_text, model_id=model.id, session=session + ) + if cached_pred and use_cache: + logger.info(f"cache loaded from prediction: {cached_pred.id}") + logger.debug(f"{cached_pred}") + + # Explicitly pass required fields to ParagraphPredictionPublic + return ParagraphPredictionPublic( + id=paragraph.id, + text=paragraph.text, + hash=paragraph.hash, + index=paragraph.index, + created_at=paragraph.created_at, + updated_at=paragraph.updated_at, + prediction=PredictionPublic( + **cached_pred.model_dump(), + model=cached_pred.model.model_dump(), + ), + ) + + # ——————— Run prediction ————————————————————————————————————————————————————————— + logger.info("Running prediction") + item = [{"path": "empty", "data": {"doc.text": input_text}}] + + with pipeline_lock: + processed = pipeline.preprocess(item) + processed = pipeline.predict_single(processed[0]) + processed = pipeline.postprocess([processed]) + + output_text = get_element(processed[0], ["data", "doc.text"]) or "" + output_labels = get_element(processed[0], ["predictions", "entities"]) or [] + + # ———————— Sanity check —————————————————————————————————————————————————————————— + if input_text != output_text: + logger.critical( + f"Input and output text do not match: {input_text} != {output_text}" + ) + if settings.ERROR_HANDLER == "raise": + raise ValueError("Input and output text do not match") + + # Run validation on the output labels + output_labels = [ + DocLabel(**label).model_dump(mode="json") for label in output_labels + ] + + # ——————— Save to cache —————————————————————————————————————————————————————————— + if pred := cached_pred: + logger.info(f"saving in cache: {pred.id}") + + logger.warning(f"Prediction already exists: {pred}. Updating.") + pred.input = output_text + pred.prediction = output_labels + else: + pred = PredictionCreate( + input=output_text, + prediction=output_labels, + validation=None, + fk_model=model.id, + fk_paragraph=paragraph.id, + ).compile() + + session.add(pred) + session.commit() + session.refresh(pred) + + return ParagraphPredictionPublic( + id=paragraph.id, + text=paragraph.text, + index=paragraph.index, + hash=paragraph.hash, + created_at=paragraph.created_at, + updated_at=paragraph.updated_at, + prediction=PredictionPublic( + **pred.model_dump(), + model=pred.model.model_dump(), + ), + ) + + +# MARK: Paragraph Validate +@router.patch("/paragraph/{paragraph_id}/pipeline/{pipeline_type}/validate") +async def save_paragraph_validation( + paragraph_id: UUID4, + pipeline_type: ModelType, + labels: list[DocLabel], + session: Session = Depends(get_session), +) -> None: + paragraph = session.get(Paragraph, paragraph_id) + + model = read_latest_model_by_type(model_type=pipeline_type, session=session) + + # ———————— Sanity check —————————————————————————————————————————————————————————— + # Check if the document exists + if not paragraph: + raise HTTPException( + status_code=404, + detail=f"Paragraph not found: {paragraph_id}", + ) + + # ——————— Updating validations ——————————————————————————————————————————————————— + prediction = read_prediction_by_text( + text=paragraph.text, model_id=model.id, session=session + ) + if prediction: + prediction.validation = labels + else: + logger.warning("Prediction not found.") + if settings.ERROR_HANDLER == "raise": + raise ValueError(f"Prediction not found for paragraph: `{paragraph.id}`") + prediction = Prediction( + input=paragraph.text, + fk_model=model.id, + validation=labels, + ) + + session.add(prediction) + session.commit() + + return diff --git a/aymurai/api/main.py b/aymurai/api/main.py index 9ea9a2a..29c63f0 100644 --- a/aymurai/api/main.py +++ b/aymurai/api/main.py @@ -6,16 +6,16 @@ from alembic import command from alembic.config import Config from fastapi import FastAPI, Request -from fastapi.openapi.docs import get_swagger_ui_html -from fastapi.responses import RedirectResponse from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse from aymurai.api import core +from aymurai.api.startup.database import check_db_connection +from aymurai.database.crud.model import ModelType, register_model +from aymurai.database.session import get_session from aymurai.logger import get_logger -from aymurai.settings import settings from aymurai.pipeline import AymurAIPipeline -from aymurai.api.startup.database import check_db_connection - +from aymurai.settings import settings, Settings logger = get_logger(__name__) @@ -28,6 +28,8 @@ @asynccontextmanager async def lifespan(app: FastAPI): logger.info("> Initializing service") + + # ------ Check DB connection and run migrations ---------------------------------- logger.info(f">> Checking DB connection: `{settings.SQLALCHEMY_DATABASE_URI}`") try: check_db_connection() @@ -37,6 +39,32 @@ async def lifespan(app: FastAPI): except Exception as error: logger.error("Error while starting up:", error) + # ------ Register pipelines ------------------------------------------------------ + logger.info(">> Registering models") + session = next(get_session()) + PIPELINES_PATH = os.path.join(RESOURCES_BASEPATH, "pipelines", "production") + model = register_model( + model_name="flair-anonymizer", + model_type=ModelType.ANONYMIZATION, + app_version=settings.APP_VERSION, + pipeline_path=os.path.join(PIPELINES_PATH, "flair-anonymizer"), + session=session, + ) + logger.info( + f">>> Registering `{model.type}` model: {model.name} (version: {model.version})" + ) + + model = register_model( + model_name="datapublic", + model_type=ModelType.DATAPUBLIC, + app_version=settings.APP_VERSION, + pipeline_path=os.path.join(PIPELINES_PATH, "full-paragraph"), + session=session, + ) + logger.info( + f">>> Registering `{model.type}` model: {model.name} (version: {model.version})" + ) + yield @@ -44,7 +72,7 @@ async def lifespan(app: FastAPI): title="AymurAI API", version=settings.APP_VERSION, lifespan=lifespan, - docs_url=None, + docs_url="/docs" if not settings.SWAGGER_UI_DARK_MODE else None, ) @@ -76,13 +104,18 @@ async def index(): return "/docs" -# @api.get("/docs", include_in_schema=False) -# async def custom_swagger_ui_html(): -# return get_swagger_ui_html( -# openapi_url=api.openapi_url, -# title=f"{api.title} - Swagger UI", -# swagger_css_url="https://cdn.jsdelivr.net/gh/danielperezrubio/swagger-dark-theme@main/assets/swagger-ui.min.css", -# ) +if settings.SWAGGER_UI_DARK_MODE: + from fastapi.openapi.docs import get_swagger_ui_html + + api.docs_url = None + + @api.get("/docs", include_in_schema=False) + async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=api.openapi_url, + title=f"{api.title} - Swagger UI", + swagger_css_url="https://cdn.jsdelivr.net/gh/danielperezrubio/swagger-dark-theme@main/assets/swagger-ui.min.css", + ) ################################################################################ @@ -96,6 +129,20 @@ def healthcheck(): return {"status": "ok"} +# API settings +if settings.DEVELOPMENT_MODE: + logger.warning("Development mode enabled") + + @api.get( + "/server/environment", + status_code=200, + tags=["server"], + response_model=Settings, + ) + def environment(): + return settings.model_dump() + + # Api endpoints api.include_router(core.router) diff --git a/aymurai/database/crud/document-bkp.py b/aymurai/database/crud/document-bkp.py new file mode 100644 index 0000000..cc97a1c --- /dev/null +++ b/aymurai/database/crud/document-bkp.py @@ -0,0 +1,71 @@ +import uuid + +from sqlmodel import Session, select + +from aymurai.logger import get_logger +from aymurai.database.schema import ( + Document, + Paragraph, + DocumentUpdate, +) + +logger = get_logger(__name__) + + +def document_create( + id: uuid.UUID, + name: str, + paragraphs: list[Paragraph], + session: Session, +) -> Document: + document = Document(id=id, name=name, paragraphs=paragraphs) + + exists = session.get(Document, id) + if exists: + logger.warning(f"Document already exists: {id}. skipping creation.") + return exists + + session.add(document) + session.commit() + session.refresh(document) + + return document + + +def document_read( + document_id: uuid.UUID, + session: Session, +) -> Document | None: + statement = select(Document).where(Document.id == document_id) + data = session.exec(statement).first() + return data + + +def document_update( + document_id: uuid.UUID, + document_in: DocumentUpdate, + session: Session, +) -> Document: + statement = select(Document).where(Document.id == document_id) + document = session.exec(statement).first() + + if not document: + raise ValueError(f"Document not found: {document_id}") + + for field, value in document_in.model_dump(exclude_unset=True).items(): + setattr(document, field, value) + + return document_create(document, session) + + +def document_delete(document_id: uuid.UUID, session: Session): + statement = select(Document).where(Document.id == document_id) + document = session.exec(statement).first() + + if not document: + raise ValueError(f"Document not found: {document_id}") + + session.delete(document) + session.commit() + + return diff --git a/aymurai/database/crud/model.py b/aymurai/database/crud/model.py index 7a39037..8a40455 100644 --- a/aymurai/database/crud/model.py +++ b/aymurai/database/crud/model.py @@ -1,17 +1,23 @@ -from sqlmodel import Session -from aymurai.database.schema import Model, ModelPublic, ModelCreate +from sqlmodel import Session, desc, select + +from aymurai.database.schema import Model, ModelCreate, ModelPublic, ModelType from aymurai.database.utils import text_to_uuid from aymurai.logger import get_logger - logger = get_logger(__name__) -def register_model(model_name: str, app_version: str, session: Session) -> ModelPublic: +def register_model( + model_name: str, + app_version: str, + model_type: ModelType, + pipeline_path: str, + session: Session, +) -> ModelPublic: """ Register a model in the database. """ - model_id = text_to_uuid(f"{model_name}-{app_version}") + model_id = text_to_uuid(f"{model_type}-{model_name}-{app_version}") # Check if the model already exists model = session.get(Model, model_id) @@ -19,10 +25,29 @@ def register_model(model_name: str, app_version: str, session: Session) -> Model logger.warning(f"Model already exists: {model_id}. skipping creation.") return ModelPublic(**model.model_dump()) - model = ModelCreate(name=model_name, version=app_version).compile() + model = ModelCreate( + id=model_id, + name=model_name, + version=app_version, + type=model_type, + pipeline_path=pipeline_path, + ).compile() session.add(model) session.commit() session.refresh(model) return ModelPublic(**model.model_dump()) + + +def read_latest_model_by_type(model_type: ModelType, session: Session) -> ModelPublic: + """ + Read a model from the database by its type. + """ + stm = select(Model).where(Model.type == model_type).order_by(desc(Model.created_at)) + model = session.exec(stm).first() + + if not model: + raise ValueError(f"Model not found for type: {model_type}") + + return ModelPublic(**model.model_dump()) diff --git a/aymurai/database/crud/prediction.py b/aymurai/database/crud/prediction.py index d2993a4..ac7b424 100644 --- a/aymurai/database/crud/prediction.py +++ b/aymurai/database/crud/prediction.py @@ -1,9 +1,18 @@ import uuid -from sqlmodel import Session, select -from aymurai.database.schema import Prediction +from sqlmodel import Session, select, func +from sqlalchemy.orm import aliased +from aymurai.database.schema import ( + Prediction, + Model, + Paragraph, + PredictionPublic, + ModelType, +) +from aymurai.database.meta.extra import ( + ParagraphPredictionPublic, +) from aymurai.database.utils import text_to_hash -from aymurai.database.schema import Model def read_validation( @@ -40,7 +49,7 @@ def read_validation( return pred or None -def read_prediction( +def read_prediction_by_text( text: str, model_id: uuid.UUID, session: Session, @@ -71,3 +80,71 @@ def read_prediction( pred = session.exec(statement).first() return pred or None + + +def get_paragraph_prediction_public( + paragraph_pred: tuple[Paragraph, PredictionPublic | None], +) -> ParagraphPredictionPublic: + paragraph, pred = paragraph_pred + + if pred is None: + return ParagraphPredictionPublic(**paragraph.model_dump(), prediction=None) + + obj = ParagraphPredictionPublic( + **paragraph.model_dump(), + prediction=PredictionPublic( + **pred.model_dump(), + model=pred.model.model_dump(), + ), + ) + + return obj + + +def read_document_prediction_paragraphs( + document_id: uuid.UUID, + model_type: ModelType, + session: Session, +) -> list[ParagraphPredictionPublic]: + """ + For each paragraph in `document_id` (ordered by Paragraph.index) + and for the given `model_type`, return the most‐recent Prediction or None. + """ + + # 1) build a row_number window over each paragraph + rn = ( + func.row_number() + .over( + partition_by=Prediction.fk_paragraph, + order_by=(Prediction.updated_at.desc(), Prediction.created_at.desc()), + ) + .label("rn") + ) + + # 2) subquery: join to Model by type & compute rn + pred_subq = ( + select(Prediction, rn) + .join(Model, Prediction.fk_model == Model.id) + .where(Model.type == model_type) + ).subquery() + + # alias the subquery as a full ORM-mapped Prediction + LatestPred = aliased(Prediction, pred_subq) + + # 3) outer-join Paragraph → the aliased Prediction, filter rn==1, order by paragraph.index + stmt = ( + select(Paragraph, LatestPred) + .join( + LatestPred, + Paragraph.id == LatestPred.fk_paragraph, + isouter=True, + ) + .where(Paragraph.fk_document == document_id) + .where((pred_subq.c.rn == 1) | (pred_subq.c.rn.is_(None))) + .order_by(Paragraph.index) + ) + + rows = session.exec(stmt).all() + # rows is list of (Paragraph, LatestPred or None) + + return [get_paragraph_prediction_public(row) for row in rows] diff --git a/aymurai/database/meta/datapublic/categories.py b/aymurai/database/meta/datapublic/categories.py deleted file mode 100644 index 8a540b4..0000000 --- a/aymurai/database/meta/datapublic/categories.py +++ /dev/null @@ -1,58 +0,0 @@ -from enum import Enum - - -class Materia(str, Enum): - PENAL = "penal" - CONTRAVENCIONAL = "contravencional" - FALTAS = "faltas" - AMPARO = "amparo" - HABEAS_CORPUS = "habeas corpus" - EJECUCIONES_DE_MULTA = "ejecuciones de multa" - - -class Violencia(str, Enum): - SI = "si" - NO = "no" - SD = "s/d" - NO_CORRESPONDE = "no_corresponde" - - -class ModalidadViolencia(str, Enum): - DOMESTICA = "domestica" - INSTITUCIONAL = "institucional" - MEDIATICA = "mediatica" - LABORAL = "laboral" - CONTRA_LIBERTAD_REPRODUCTIVA = "contra la libertad reproductiva" - OBSTETRICA = "obstetrica" - ESPACIO_PUBLICO_PRIVADO = "en espacio publico o privado" - POLITICA_PUBLICA = "politica y publica" - DIGITAL = "digital" - - -class Genero(str, Enum): - VARON_CIS = "varon_cis" - MUJER_CIS = "mujer_cis" - MUJER_TRANS = "mujer_trans" - TRAVESTI = "travesti" - VARON_TRANS = "varon_trans" - NO_BINARIA = "no_binaria" - NO_CORRESPONDE = "no_corresponde" - NO_RESPONDE = "no_responde" - - -class FrecuenciaEpisodios(str, Enum): - ESPORADICO = "esporádico" - DIARIO = "diario" - HABITUAL = "habitual" - EVENTUAL = "eventual" - PRIMERA_AGRESION = "primera agresión" - - -class TipoResolucion(str, Enum): - INTERLOCUTORIA = "interlocutoria" - DEFINITIVA = "definitiva" - - -class OralEscrita(str, Enum): - ORAL = "oral" - ESCRITA = "escrita" diff --git a/aymurai/database/meta/extra.py b/aymurai/database/meta/extra.py new file mode 100644 index 0000000..a7da805 --- /dev/null +++ b/aymurai/database/meta/extra.py @@ -0,0 +1,9 @@ +from aymurai.database.schema import PredictionPublic, ParagraphPublic, DocumentPublic + + +class ParagraphPredictionPublic(ParagraphPublic): + prediction: PredictionPublic | None = None + + +class DocumentPredictionPublic(DocumentPublic): + paragraphs: list[ParagraphPredictionPublic] | None = None diff --git a/aymurai/database/meta/model.py b/aymurai/database/meta/model.py index 1031f54..7869af1 100644 --- a/aymurai/database/meta/model.py +++ b/aymurai/database/meta/model.py @@ -1,27 +1,29 @@ +import enum import uuid from datetime import datetime -from pydantic import BaseModel, model_validator +from pydantic import BaseModel from sqlalchemy import text -from sqlmodel import Field, SQLModel, Relationship -from aymurai.database.utils import text_to_uuid - +from sqlmodel import Field, Relationship, SQLModel from typing_extensions import TYPE_CHECKING if TYPE_CHECKING: from aymurai.database.meta.prediction import Prediction +class ModelType(str, enum.Enum): + ANONYMIZATION = "anonymization" + DATAPUBLIC = "datapublic" + + class ModelBase(SQLModel): - id: uuid.UUID | None = Field(None, primary_key=True) + id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) name: str = Field(nullable=False) version: str = Field(nullable=False) - @model_validator(mode="after") - def validate_name_version(self) -> "ModelBase": - self.id = text_to_uuid(f"{self.name}-{self.version}") - return self + type: ModelType = Field(nullable=False) + pipeline_path: str = Field(nullable=False) class Model(ModelBase, table=True): @@ -33,12 +35,6 @@ class Model(ModelBase, table=True): predictions: list["Prediction"] = Relationship(back_populates="model") - # @model_validator(mode="before") - # def generate_id(cls, values: dict): - # if values.get("id") is None and set(values.keys()) & {"name", "version"}: - # values["id"] = text_to_uuid(f"{values['name']}-{values['version']}") - # return values - class ModelCreate(ModelBase): def compile(self) -> Model: @@ -47,7 +43,10 @@ def compile(self) -> Model: class ModelPublic(BaseModel): id: uuid.UUID + + type: ModelType name: str version: str + pipeline_path: str created_at: datetime diff --git a/aymurai/database/meta/paragraph.py b/aymurai/database/meta/paragraph.py index 307e409..8230d15 100644 --- a/aymurai/database/meta/paragraph.py +++ b/aymurai/database/meta/paragraph.py @@ -1,15 +1,17 @@ import uuid from datetime import datetime -from pydantic import BaseModel, model_validator, computed_field -from typing_extensions import TYPE_CHECKING, Self -from sqlmodel import Field, SQLModel, Relationship +from pydantic import BaseModel, computed_field, model_validator from sqlalchemy import Column, DateTime, func, text +from sqlmodel import Field, Relationship, SQLModel +from typing_extensions import TYPE_CHECKING, Self from aymurai.database.utils import text_to_uuid +# from aymurai.database.schema import PredictionPublic + if TYPE_CHECKING: - from aymurai.database.schema import Document, Prediction + from aymurai.database.schema import Document, Prediction, PredictionPublic class ParagraphBase(SQLModel): @@ -17,12 +19,18 @@ class ParagraphBase(SQLModel): text: str = Field(nullable=False) + index: int = Field( + nullable=False, + description="Index of the paragraph in the document", + ) + @model_validator(mode="after") def validate_text(self) -> Self: self.id = text_to_uuid(self.text) return self @computed_field + @property def hash(self) -> uuid.UUID: """Compute the hash of the text""" return text_to_uuid(self.text) @@ -54,6 +62,7 @@ class ParagraphPublic(BaseModel): text: str hash: uuid.UUID + index: int created_at: datetime updated_at: datetime | None diff --git a/aymurai/database/meta/prediction.py b/aymurai/database/meta/prediction.py index 5565855..b3da7fd 100644 --- a/aymurai/database/meta/prediction.py +++ b/aymurai/database/meta/prediction.py @@ -1,18 +1,16 @@ import uuid from datetime import datetime -from pydantic import BaseModel, model_validator -from sqlmodel import Field, SQLModel, Relationship +from pydantic import BaseModel, computed_field, model_validator from sqlalchemy import JSON, Column, DateTime, func, text - -from aymurai.meta.api_interfaces import DocLabel +from sqlmodel import Field, Relationship, SQLModel from aymurai.database.meta.model import ModelPublic -from typing import TYPE_CHECKING -from aymurai.database.utils import text_to_hash # if TYPE_CHECKING: from aymurai.database.schema import Model, Paragraph +from aymurai.database.utils import text_to_hash +from aymurai.meta.api_interfaces import DocLabel class PredictionBase(SQLModel): @@ -62,7 +60,13 @@ class PredictionPublic(BaseModel): updated_at: datetime | None = None input: str - prediction: list[DocLabel] = Field(default_factory=list) - validation: list[DocLabel] = Field(default_factory=list) + prediction: list[DocLabel] | None = Field(None, exclude=True) + validation: list[DocLabel] | None = Field(None, exclude=True) model: ModelPublic + + @computed_field + @property + def labels(self) -> list[DocLabel]: + """Return the labels of the prediction""" + return self.validation or self.prediction or [] diff --git a/aymurai/database/schema.py b/aymurai/database/schema.py index 9d32c43..c2c7c97 100644 --- a/aymurai/database/schema.py +++ b/aymurai/database/schema.py @@ -1,43 +1,32 @@ # ruff: noqa: F401 -from .meta.datapublic.paragraph import ( - DataPublicParagraph, - DataPublicParagraphRead, - DataPublicParagraphCreate, - DataPublicParagraphUpdate, -) -from .meta.datapublic.document import ( - DataPublicDocumentBase, - DataPublicDocument, - DataPublicDocumentRead, - DataPublicDocumentCreate, - DataPublicDocumentUpdate, -) -from .meta.anonymization.paragraph import ( - AnonymizationParagraph, - AnonymizationParagraphRead, - AnonymizationParagraphCreate, - AnonymizationParagraphUpdate, -) from .meta.anonymization.document import ( AnonymizationDocument, - AnonymizationDocumentRead, AnonymizationDocumentCreate, - AnonymizationDocumentUpdate, AnonymizationDocumentParagraph, + AnonymizationDocumentRead, + AnonymizationDocumentUpdate, ) - -from .meta.document import ( - Document, - DocumentPublic, - DocumentUpdate, +from .meta.anonymization.paragraph import ( + AnonymizationParagraph, + AnonymizationParagraphCreate, + AnonymizationParagraphRead, + AnonymizationParagraphUpdate, ) -from .meta.paragraph import ( - Paragraph, - ParagraphPublic, - ParagraphUpdate, +from .meta.datapublic.document import ( + DataPublicDocument, + DataPublicDocumentBase, + DataPublicDocumentCreate, + DataPublicDocumentRead, + DataPublicDocumentUpdate, ) -from .meta.model import Model, ModelPublic, ModelCreate -from .meta.prediction import Prediction, PredictionPublic, PredictionCreate -from .meta.datapublic.document_paragraph import ( - DataPublicDocumentParagraph, +from .meta.datapublic.document_paragraph import DataPublicDocumentParagraph +from .meta.datapublic.paragraph import ( + DataPublicParagraph, + DataPublicParagraphCreate, + DataPublicParagraphRead, + DataPublicParagraphUpdate, ) +from .meta.document import Document, DocumentPublic, DocumentUpdate +from .meta.model import Model, ModelCreate, ModelPublic, ModelType +from .meta.paragraph import Paragraph, ParagraphPublic, ParagraphUpdate +from .meta.prediction import Prediction, PredictionCreate, PredictionPublic diff --git a/aymurai/database/versions/6a418ffd84da_create_database.py b/aymurai/database/versions/6a418ffd84da_create_database.py deleted file mode 100644 index 93f39c1..0000000 --- a/aymurai/database/versions/6a418ffd84da_create_database.py +++ /dev/null @@ -1,400 +0,0 @@ -"""Create database - -Revision ID: 6a418ffd84da -Revises: -Create Date: 2025-04-05 02:50:55.031170 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import sqlmodel - - -# revision identifiers, used by Alembic. -revision: str = "6a418ffd84da" -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "anonymization_document", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "anonymization_paragraph", - sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("prediction", sa.JSON(), nullable=True), - sa.Column("validation", sa.JSON(), nullable=True), - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "datapublic_document", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "datapublic_paragraph", - sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("prediction", sa.JSON(), nullable=True), - sa.Column("validation", sa.JSON(), nullable=True), - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "document", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("data", sa.LargeBinary(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "model", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "anonymization_document_paragraph", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("document_id", sa.Uuid(), nullable=False), - sa.Column("paragraph_id", sa.Uuid(), nullable=False), - sa.Column("order", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ["document_id"], - ["anonymization_document.id"], - ), - sa.ForeignKeyConstraint( - ["paragraph_id"], - ["anonymization_paragraph.id"], - ), - sa.PrimaryKeyConstraint("id", "document_id", "paragraph_id"), - ) - op.create_table( - "datapublic_dataset", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("nro_registro", sa.Integer(), nullable=True), - sa.Column("fecha_resolucion", sa.Date(), nullable=True), - sa.Column("n_expte_eje", sa.Integer(), nullable=True), - sa.Column("cuij", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("firma", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column( - "materia", - sa.Enum( - "PENAL", - "CONTRAVENCIONAL", - "FALTAS", - "AMPARO", - "HABEAS_CORPUS", - "EJECUCIONES_DE_MULTA", - name="materia", - ), - nullable=True, - ), - sa.Column("art_infringido", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("codigo_o_ley", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("conducta", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column( - "conducta_descripcion", sqlmodel.sql.sqltypes.AutoString(), nullable=True - ), - sa.Column("violencia_de_genero", sa.Boolean(), nullable=True), - sa.Column( - "v_fisica", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "v_psic", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "v_econ", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "v_sex", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "v_soc", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "v_amb", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "v_simb", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "v_polit", - sa.Enum("SI", "NO", "SD", "NO_CORRESPONDE", name="violencia"), - nullable=True, - ), - sa.Column( - "modalidad_de_la_violencia", - sa.Enum( - "DOMESTICA", - "INSTITUCIONAL", - "MEDIATICA", - "LABORAL", - "CONTRA_LIBERTAD_REPRODUCTIVA", - "OBSTETRICA", - "ESPACIO_PUBLICO_PRIVADO", - "POLITICA_PUBLICA", - "DIGITAL", - name="modalidadviolencia", - ), - nullable=True, - ), - sa.Column("frases_agresion", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column( - "genero_acusado_a", - sa.Enum( - "VARON_CIS", - "MUJER_CIS", - "MUJER_TRANS", - "TRAVESTI", - "VARON_TRANS", - "NO_BINARIA", - "NO_CORRESPONDE", - "NO_RESPONDE", - name="genero", - ), - nullable=True, - ), - sa.Column( - "persona_acusada_no_determinada", - sqlmodel.sql.sqltypes.AutoString(), - nullable=True, - ), - sa.Column( - "nacionalidad_acusado_a", sqlmodel.sql.sqltypes.AutoString(), nullable=True - ), - sa.Column("edad_acusado_a_al_momento_del_hecho", sa.Integer(), nullable=True), - sa.Column( - "nivel_de_instruccion_acusado_a", - sqlmodel.sql.sqltypes.AutoString(), - nullable=True, - ), - sa.Column( - "genero_denunciante", - sa.Enum( - "VARON_CIS", - "MUJER_CIS", - "MUJER_TRANS", - "TRAVESTI", - "VARON_TRANS", - "NO_BINARIA", - "NO_CORRESPONDE", - "NO_RESPONDE", - name="genero", - ), - nullable=True, - ), - sa.Column( - "nacionalidad_denunciante", - sqlmodel.sql.sqltypes.AutoString(), - nullable=True, - ), - sa.Column("edad_denunciante_al_momento_del_hecho", sa.Integer(), nullable=True), - sa.Column( - "nivel_de_instruccion_denunciante", - sqlmodel.sql.sqltypes.AutoString(), - nullable=True, - ), - sa.Column("trabajo_remunerado_denunciante", sa.Boolean(), nullable=True), - sa.Column("nivel_de_ingresos_denunciante", sa.Float(), nullable=True), - sa.Column( - "domicilio_denunciante", sqlmodel.sql.sqltypes.AutoString(), nullable=True - ), - sa.Column("asentamiento_o_villa", sa.Boolean(), nullable=True), - sa.Column( - "frecuencia_episodios", - sa.Enum( - "ESPORADICO", - "DIARIO", - "HABITUAL", - "EVENTUAL", - "PRIMERA_AGRESION", - name="frecuenciaepisodios", - ), - nullable=True, - ), - sa.Column( - "relacion_y_tipo_entre_acusado_a_y_denunciante", - sqlmodel.sql.sqltypes.AutoString(), - nullable=True, - ), - sa.Column("hijos_hijas_en_comun", sa.Boolean(), nullable=True), - sa.Column( - "medidas_de_proteccion_vigentes_al_momento_del_hecho", - sa.Boolean(), - nullable=True, - ), - sa.Column("zona_del_hecho", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("lugar_del_hecho", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("fecha_del_hecho", sa.Date(), nullable=True), - sa.Column("fecha_de_inicio_del_hecho", sa.Date(), nullable=True), - sa.Column("fecha_de_finalizacion_de_hecho", sa.Date(), nullable=True), - sa.Column( - "tipo_de_resolucion", - sa.Enum("INTERLOCUTORIA", "DEFINITIVA", name="tiporesolucion"), - nullable=True, - ), - sa.Column( - "objeto_de_la_resolucion", sqlmodel.sql.sqltypes.AutoString(), nullable=True - ), - sa.Column("detalle", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("decision", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column( - "oral_escrita", - sa.Enum("ORAL", "ESCRITA", name="oralescrita"), - nullable=True, - ), - sa.Column("hora_de_inicio", sa.Time(), nullable=True), - sa.Column("hora_de_cierre", sa.Time(), nullable=True), - sa.Column("duracion", sa.Interval(), nullable=True), - sa.Column("link", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("document_id", sa.Uuid(), nullable=True), - sa.ForeignKeyConstraint( - ["document_id"], - ["datapublic_document.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "datapublic_document_paragraph", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("document_id", sa.Uuid(), nullable=False), - sa.Column("paragraph_id", sa.Uuid(), nullable=False), - sa.Column("order", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ["document_id"], - ["datapublic_document.id"], - ), - sa.ForeignKeyConstraint( - ["paragraph_id"], - ["datapublic_paragraph.id"], - ), - sa.PrimaryKeyConstraint("id", "document_id", "paragraph_id"), - ) - op.create_table( - "paragraph", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.Column("fk_document", sa.Uuid(), nullable=False), - sa.ForeignKeyConstraint( - ["fk_document"], - ["document.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "prediction", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("input", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("prediction", sa.JSON(), nullable=True), - sa.Column("validation", sa.JSON(), nullable=True), - sa.Column("fk_model", sa.Uuid(), nullable=False), - sa.Column("fk_paragraph", sa.Uuid(), nullable=True), - sa.Column("input_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint( - ["fk_model"], - ["model.id"], - ), - sa.ForeignKeyConstraint( - ["fk_paragraph"], - ["paragraph.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("prediction") - op.drop_table("paragraph") - op.drop_table("datapublic_document_paragraph") - op.drop_table("datapublic_dataset") - op.drop_table("anonymization_document_paragraph") - op.drop_table("model") - op.drop_table("document") - op.drop_table("datapublic_paragraph") - op.drop_table("datapublic_document") - op.drop_table("anonymization_paragraph") - op.drop_table("anonymization_document") - # ### end Alembic commands ### diff --git a/aymurai/database/versions/13f78d08e925_create_database.py b/aymurai/database/versions/d6c141e03875_create_database.py similarity index 57% rename from aymurai/database/versions/13f78d08e925_create_database.py rename to aymurai/database/versions/d6c141e03875_create_database.py index b01416e..bc1d5a4 100644 --- a/aymurai/database/versions/13f78d08e925_create_database.py +++ b/aymurai/database/versions/d6c141e03875_create_database.py @@ -1,8 +1,8 @@ """Create database -Revision ID: 13f78d08e925 +Revision ID: d6c141e03875 Revises: -Create Date: 2025-05-01 00:15:10.748811 +Create Date: 2025-05-12 15:56:52.453998 """ from typing import Sequence, Union @@ -13,7 +13,7 @@ # revision identifiers, used by Alembic. -revision: str = "13f78d08e925" +revision: str = "d6c141e03875" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -78,6 +78,39 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint("id"), ) + op.create_table( + "document", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("data", sa.LargeBinary(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "model", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "type", + sa.Enum("ANONYMIZATION", "DATAPUBLIC", name="modeltype"), + nullable=False, + ), + sa.Column("pipeline_path", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) op.create_table( "anonymization_document_paragraph", sa.Column("id", sa.Uuid(), nullable=False), @@ -110,13 +143,62 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id", "document_id", "paragraph_id"), ) + op.create_table( + "paragraph", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("index", sa.Integer(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("fk_document", sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint( + ["fk_document"], + ["document.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "prediction", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("input", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("prediction", sa.JSON(), nullable=True), + sa.Column("validation", sa.JSON(), nullable=True), + sa.Column("fk_model", sa.Uuid(), nullable=False), + sa.Column("fk_paragraph", sa.Uuid(), nullable=True), + sa.Column("input_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["fk_model"], + ["model.id"], + ), + sa.ForeignKeyConstraint( + ["fk_paragraph"], + ["paragraph.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("prediction") + op.drop_table("paragraph") op.drop_table("datapublic_document_paragraph") op.drop_table("anonymization_document_paragraph") + op.drop_table("model") + op.drop_table("document") op.drop_table("datapublic_paragraph") op.drop_table("datapublic_document") op.drop_table("anonymization_paragraph") diff --git a/aymurai/meta/api_interfaces.py b/aymurai/meta/api_interfaces.py index 9d9d7dd..49089e7 100644 --- a/aymurai/meta/api_interfaces.py +++ b/aymurai/meta/api_interfaces.py @@ -2,7 +2,7 @@ from pydantic import UUID5, BaseModel, Field, RootModel -from aymurai.meta.entities import EntityAttributes +from aymurai.meta.entities import DocLabel class SuccessResponse(BaseModel): @@ -19,27 +19,11 @@ class TextRequest(BaseModel): ) -class DocLabel(BaseModel): - """Datatype for a document label""" - - text: str = Field( - description="raw text of entity", - # alias=AliasChoices(["text", "document"]), - ) - start_char: int = Field( - description="start character of the span in relation of the full text" - ) - end_char: int = Field( - description="last character of the span in relation of the full text" - ) - attrs: EntityAttributes - - class DocumentInformation(BaseModel): """Datatype for a document information with all labels""" document: str = Field(description="processed text") - labels: list[DocLabel] = Field(default_factory=list) + labels: list[DocLabel] | None = None class DocumentAnnotations(BaseModel): diff --git a/aymurai/meta/entities.py b/aymurai/meta/entities.py index d16c0b6..951609f 100644 --- a/aymurai/meta/entities.py +++ b/aymurai/meta/entities.py @@ -38,3 +38,19 @@ class Entity(BaseModel): context_pre: str = "" context_post: str = "" attrs: EntityAttributes | None = None + + +class DocLabel(BaseModel): + """Datatype for a document label""" + + text: str = Field( + description="raw text of entity", + # alias=AliasChoices(["text", "document"]), + ) + start_char: int = Field( + description="start character of the span in relation of the full text" + ) + end_char: int = Field( + description="last character of the span in relation of the full text" + ) + attrs: EntityAttributes diff --git a/aymurai/meta/xml_document.py b/aymurai/meta/xml_document.py new file mode 100644 index 0000000..250256c --- /dev/null +++ b/aymurai/meta/xml_document.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel + +from aymurai.database.meta.extra import ParagraphPredictionPublic + + +class XMLTextFragment(BaseModel): + text: str + normalized_text: str + start: int + end: int + fragment_index: int + paragraph_index: int + + +class ParagraphMetadata(BaseModel): + start: int + end: int + fragments: list[XMLTextFragment] + xml_file: str + + +class XMLParagraph(BaseModel): + plain_text: str + metadata: ParagraphMetadata + hash: str | int | None = None + __pred_indices: list[int] = [] + + +class XMLParagraphWithParagraphPrediction(XMLParagraph): + paragraph_prediction: ParagraphPredictionPublic diff --git a/aymurai/settings.py b/aymurai/settings.py index 6e6ebc3..b364592 100644 --- a/aymurai/settings.py +++ b/aymurai/settings.py @@ -3,9 +3,9 @@ from typing import Literal from dotenv import load_dotenv +from pydantic import ConfigDict, FilePath, field_validator from pydantic_settings import BaseSettings -from pydantic import FilePath, ConfigDict, field_validator - +from aymurai.logger import get_logger import aymurai try: @@ -15,15 +15,20 @@ PARENT = Path(aymurai.__file__).parent +logger = get_logger(__name__) + def load_env(): + logger.info("Loading environment variables from .env files") load_dotenv(".env") # Load the stage-specific .env file (if it exists) stage = os.getenv("STAGE") if stage: + logger.info(f"Loading environment variables for stage: {stage}") env_file = f".env.{stage}" if os.path.exists(env_file): + logger.info(f"Loading environment variables from {env_file}") load_dotenv(env_file) @@ -71,6 +76,10 @@ def assemble_cors_origins(cls, v) -> list[str]: LIBREOFFICE_BIN: str = "libreoffice" + # ----- Miscellaneous settings ----- + SWAGGER_UI_DARK_MODE: bool = False + DEVELOPMENT_MODE: bool = False + load_env() settings = Settings() diff --git a/aymurai/api/endpoints/routers/misc/__init__.py b/aymurai/text/anonymization/__init__.py similarity index 100% rename from aymurai/api/endpoints/routers/misc/__init__.py rename to aymurai/text/anonymization/__init__.py diff --git a/aymurai/text/anonymization/alignment.py b/aymurai/text/anonymization/alignment.py new file mode 100644 index 0000000..5cf14a8 --- /dev/null +++ b/aymurai/text/anonymization/alignment.py @@ -0,0 +1,390 @@ +import os +import re +import xml.sax.saxutils +from copy import deepcopy +from unicodedata import normalize + +import jiwer +import numpy as np +import pandas as pd +from more_itertools import flatten + +from aymurai.database.meta.extra import ParagraphPredictionPublic +from aymurai.database.schema import PredictionPublic +from aymurai.meta.entities import DocLabel, EntityAttributes +from aymurai.meta.xml_document import ( + ParagraphMetadata, + XMLParagraph, + XMLParagraphWithParagraphPrediction, + XMLTextFragment, +) +from aymurai.models.flair.utils import FlairTextNormalize +from aymurai.utils.alignment.core import align_text, tokenize + +REGEX_PARAGRAPH = r"((?.*?)(\/w:p\b)" +REGEX_FRAGMENT = r"(?(?P.*?)(<.*?\/w:t)" + + +def merge_consecutive_labels(text: str, labels: list[DocLabel]) -> list[DocLabel]: + """ + Merge consecutive labels in the paragraph prediction. + Args: + paragraph (XMLParagraphWithParagraphPrediction): A dictionary representing the prediction. + labels (list[DocLabel]): A list of labels to merge. + Returns: + list[DocLabel]: A list of dictionaries representing the joined labels. + """ + + # Reorder labels based on start indices + labels = sorted(labels, key=lambda x: x.start_char) + + # merge_labels = [] + # pivot_label_attrs = None + + # # Iterate over labels + # for label in labels: + # # Get attributes + # label_text = label.attrs.aymurai_alt_text or label.text + # label_start_char = label.attrs.aymurai_alt_start_char or label.start_char + # label_end_char = label.attrs.aymurai_alt_end_char or label.end_char + # label_aymurai_label = label.attrs.aymurai_label + + # # Start a new group with the current label + # if pivot_label_attrs is None: + # pivot_label_attrs = label.attrs + # pivot_label_attrs.text = label_text + # pivot_label_attrs.start_char = label_start_char + # pivot_label_attrs.end_char = label_end_char + # pivot_label_attrs.aymurai_label = label_aymurai_label + + # # Extend the current group with the current label + # elif ( + # pivot_label_attrs.attrs.aymurai_label == label_aymurai_label + # and (label_start_char - pivot_label_attrs.end_char) <= 1 + # ): + # pivot_label_attrs.end_char = label_end_char + + # # Finish the current group and start a new one + # else: + # pivot_label_attrs.text = text[ + # pivot_label_attrs.start_char : pivot_label_attrs.end_char + 1 + # ] + # merge_labels.append(pivot_label_attrs) + + # pivot_label_attrs = label + # pivot_label_attrs.text = label_text + # pivot_label_attrs.start_char = label_start_char + # pivot_label_attrs.end_char = label_end_char + # pivot_label_attrs.attrs.aymurai_label = label_aymurai_label + + # # Finish the last group + # if pivot_label_attrs is not None: + # pivot_label_attrs.text = text[ + # pivot_label_attrs.start_char : pivot_label_attrs.end_char + 1 + # ] + # merge_labels.append(pivot_label_attrs) + + unified_labels = [] + current_group = None + + # Iterate over labels + for label in labels: + label = label.model_dump() + # Get attributes + label_text = label["attrs"]["aymurai_alt_text"] or label["text"] + start_char = label["attrs"]["aymurai_alt_start_char"] or label["start_char"] + end_char = label["attrs"]["aymurai_alt_end_char"] or label["end_char"] + aymurai_label = label["attrs"]["aymurai_label"] + + if current_group is None: + # Start a new group with the current label + current_group = { + "text": label_text, + "start_char": start_char, + "end_char": end_char, + "aymurai_label": aymurai_label, + } + elif ( + current_group["aymurai_label"] == aymurai_label + and (start_char - current_group["end_char"]) <= 1 + ): + # Extend the current group with the current label + current_group["end_char"] = end_char + else: + # Finish the current group and start a new one + current_group["text"] = text[ + current_group["start_char"] : current_group["end_char"] + 1 + ] + unified_labels.append(current_group) + current_group = { + "text": label_text, + "start_char": start_char, + "end_char": end_char, + "aymurai_label": aymurai_label, + } + + # Finish the last group + if current_group is not None: + current_group["text"] = text[ + current_group["start_char"] : current_group["end_char"] + 1 + ] + unified_labels.append(current_group) + + # return unified_labels + return [ + DocLabel( + text=label["text"], + start_char=label["start_char"], + end_char=label["end_char"], + attrs=EntityAttributes( + aymurai_label=label["aymurai_label"], + aymurai_alt_text=label["text"], + aymurai_alt_start_char=label["start_char"], + aymurai_alt_end_char=label["end_char"], + ), + ) + for label in unified_labels + ] + + # return merge_labels + + +def replace_labels_in_text(text: str, prediction: PredictionPublic | None) -> str: + """ + Replaces labels in the text with anonymized tokens. + + Args: + text (str): The text to be anonymized. + prediction (PredictionPublic): The prediction object containing labels. + + Returns: + str: The text with replaced labels. + """ + + if not prediction: + return text + + # merge consecutive labels + # merged_labels = merge_consecutive_labels(text, prediction.labels) + merged_labels = prediction.labels + + # Initialize the offset + offset = 0 + + # Replace labels in the text + for label in merged_labels: + # Adjust start and end character indices of the label + start_char = label.start_char + offset + end_char = label.end_char + offset + len_text_to_replace = end_char - start_char + + # Replace the text with the anonymized token + aymurai_label = xml.sax.saxutils.escape(f" <{label.attrs.aymurai_label}>") + len_aymurai_label = len(aymurai_label) + + text = text[:start_char] + aymurai_label + text[end_char:] + + # Update the offset + offset += len_aymurai_label - len_text_to_replace + + return re.sub(r" +", " ", text).strip() + + +def erase_duplicates_justseen(series: pd.Series) -> pd.Series: + """ + Replaces consecutive duplicate values in a pandas Series with an empty string, keeping only the first occurrence. + """ + return series.where(series.ne(series.shift(), fill_value=None), "") + + +def gen_alignment_table(paragraph: XMLParagraphWithParagraphPrediction) -> pd.DataFrame: + original_text = " ".join([f.text for f in paragraph.metadata.fragments]) + anonymized_text = replace_labels_in_text( + text=paragraph.plain_text, + prediction=paragraph.paragraph_prediction.prediction, + ) + + aligned = align_text( + " " + original_text + " ", + " " + anonymized_text + " ", + ) + aligned["target"] = erase_duplicates_justseen(aligned["target"]) + + xml_file = paragraph.metadata.xml_file + + tokens = [] + for i, fragment in enumerate(paragraph.metadata.fragments): + text = fragment.text + tokenized_text = tokenize(text) + paragraph_index = fragment.paragraph_index + + # Use re.finditer to locate each instance of tokens in the text + # \S+ matches any non-whitespace sequence + token_matches = list(re.finditer(r"\S+", text)) + + # Loop over tokenized text and token_matches in parallel + for j, (token, match) in enumerate(zip(tokenized_text, token_matches)): + start = paragraph.metadata.start + fragment.start + match.start() + end = start + len(token) + + # tokens.append((xml_file, paragraph_index, i, j, token, start, end)) + tokens.append( + { + "xml_file": xml_file, + "paragraph_index": paragraph_index, + "fragment_index": i, + "token_index": j, + "token": token, + "start_char": start, + "end_char": end, + } + ) + + tokens_df = pd.DataFrame(tokens) + + tokens_df = pd.concat( + [ + tokens_df, + aligned["target"].iloc[1:-1].reset_index(drop=True), + ], + axis=1, + ) + + tokens_df["target"].fillna("", inplace=True) + + return tokens_df + + +def index_paragraphs(file: str) -> list[XMLParagraph]: + """ + Indexes the paragraphs of an XML file. + + Args: + file (str): The path to the XML file to be indexed. + + Returns: + list[Paragraph]: A list of Paragraph instances representing the indexed paragraphs. + """ + # Read the XML file + with open(file) as f: + xml = f.read() + + paragraphs = [] + paragraph_index = 0 + + # Find all paragraphs in the XML file + for match in re.finditer(REGEX_PARAGRAPH, xml): + paragraph = match.group("paragraph") + paragraph_start = match.start("paragraph") + paragraph_end = match.end("paragraph") + fragments: list[XMLTextFragment] = [] + fragment_index = 0 + + # Find all text fragments in the paragraph + for fragment in re.finditer(REGEX_FRAGMENT, paragraph): + text = fragment.group("text") + start = fragment.start("text") + end = fragment.end("text") + + fragments.append( + XMLTextFragment( + text=text, + normalized_text=FlairTextNormalize.normalize_text(text), + start=start, + end=end, + fragment_index=fragment_index, + paragraph_index=paragraph_index, + ) + ) + fragment_index += 1 + + # Join all fragments as plain text + plain_text = "".join([fragment.normalized_text for fragment in fragments]) + + paragraphs.append( + XMLParagraph( + plain_text=plain_text, + metadata=ParagraphMetadata( + start=paragraph_start, + end=paragraph_end, + fragments=fragments, + xml_file=os.path.basename(file), + ), + ) + ) + paragraph_index += 1 + + return paragraphs + + +def match_paragraphs_with_predictions( + src_paragraphs: list[XMLParagraph], + pred_paragraphs: list[ParagraphPredictionPublic], +) -> list[XMLParagraphWithParagraphPrediction]: + """ + Matches each paragraph with its corresponding predictions. + + Args: + paragraphs (list[dict]): A list of dictionaries representing the paragraphs. + predictions (list[ParagraphPredictionPublic]): A list of ParagraphPredictionPublic + instances representing the predictions. + + Returns: + list[dict]: A list of dictionaries representing + the matched paragraphs with predictions. + """ + + src_paragraphs = deepcopy(src_paragraphs) + + # Hash prediction documents + def text_hash(text: str) -> int: + return hash(normalize("NFKC", text.strip())) + + # Compute hashes for prediction paragraphs + pred_hashes = [text_hash(pred.text) for pred in pred_paragraphs] + idx2hash = {i: h for i, h in enumerate(pred_hashes)} + + # Compute hashes for source paragraphs + for p in src_paragraphs: + p.hash = text_hash(p.plain_text) + + # Map paragraph hashes to prediction indices + hash2idx = { + p.hash: [i for i, h in idx2hash.items() if h == p.hash] for p in src_paragraphs + } + + # Assign prediction indices to each paragraph + for p in src_paragraphs: + p.__pred_indices = hash2idx[p.hash] + + # Find indices of predictions that have not been matched to any paragraph + matched_indices = set(flatten(hash2idx.values())) + all_indices = set(idx2hash.keys()) + missing_indices = list(all_indices - matched_indices) + + if missing_indices: + # Assign prediction indices to each paragraph by lowest CER + target_texts = np.array([pred.text for pred in pred_paragraphs])[ + missing_indices + ] + + missing_paragraphs = [p for p in src_paragraphs if not p.__pred_indices] + + for missing_paragraph in missing_paragraphs: + source_text = missing_paragraph.plain_text + cer = np.array( + [jiwer.cer(source_text, target_text) for target_text in target_texts] + ) + min_cer_idx = np.argmin(cer) + missing_paragraph.__pred_indices = [missing_indices[min_cer_idx]] + + # Assign document text and labels + new_paragraphs = [ + XMLParagraphWithParagraphPrediction( + **p.model_dump(), + paragraph_prediction=pred_paragraphs[p.__pred_indices[0]], + ) + for p in src_paragraphs + ] + + return new_paragraphs diff --git a/aymurai/text/anonymization/core.py b/aymurai/text/anonymization/core.py new file mode 100644 index 0000000..ccb3e7a --- /dev/null +++ b/aymurai/text/anonymization/core.py @@ -0,0 +1,70 @@ +import os +import tempfile +from glob import glob + +from more_itertools import flatten + +from aymurai.database.meta.extra import ParagraphPredictionPublic +from aymurai.logger import get_logger +from aymurai.text.anonymization.alignment import ( + index_paragraphs, + match_paragraphs_with_predictions, +) +from aymurai.text.anonymization.xml_docx import ( + create_docx, + replace_text_in_xml, + unzip_document, +) + +logger = get_logger(__file__) + + +def anonymize_docx( + path: str, + paragraph_preds: list[ParagraphPredictionPublic], + output_dir: str = ".", +) -> None: + """ + Performs the anonymization process on a document. + + Args: + path (str): The path to the document to be anonymized. + paragraph_preds (list[ParagraphPredictionPublic]): The list of predictions for the document. + output_dir (str, optional): The directory to save the anonymized document. + Defaults to ".". + + Raises: + ValueError: If the document has an extension other than `.docx`. + """ + if not os.path.splitext(path)[-1] == ".docx": + raise ValueError("Only `.docx` extension is allowed.") + + if not os.path.exists(path): + raise ValueError(f"File not found: {path}") + + # Unzip document into a temporary directory + with tempfile.TemporaryDirectory() as tempdir: + unzip_document(path, tempdir) + + # Parse XML files + xml_files = glob(f"{tempdir}/**/*.xml", recursive=True) + source_paragraphs = (index_paragraphs(file) for file in xml_files) + source_paragraphs = list(flatten(source_paragraphs)) + + # Filter out empty paragraphs + source_paragraphs = [p for p in source_paragraphs if p.plain_text.strip()] + + # Matching + paragraphs = match_paragraphs_with_predictions( + source_paragraphs, paragraph_preds + ) + + # Edit XML files + replace_text_in_xml(paragraphs, tempdir) + + # Recreate anonymized document + os.makedirs(output_dir, exist_ok=True) + create_docx( + tempdir, + f"{output_dir}/{os.path.basename(path)}", + ) diff --git a/aymurai/text/anonymization/xml_docx.py b/aymurai/text/anonymization/xml_docx.py new file mode 100644 index 0000000..18c7871 --- /dev/null +++ b/aymurai/text/anonymization/xml_docx.py @@ -0,0 +1,160 @@ +import os +import re +import zipfile + +import pandas as pd +from lxml import etree + +from aymurai.meta.xml_document import XMLParagraphWithParagraphPrediction +from aymurai.text.anonymization.alignment import gen_alignment_table + + +def unzip_document(doc_path: str, output_dir: str) -> None: + """ + Unzips the document file to the specified output directory. + + Args: + doc_path (str): The path to the document file. + output_dir (str): The directory where the contents of the document + file will be extracted. + """ + # Ensure the output directory exists + os.makedirs(output_dir, exist_ok=True) + + # Open the doc file as a zip file + with zipfile.ZipFile(doc_path, "r") as doc_zip: + # Extract all the contents to the output directory + doc_zip.extractall(output_dir) + # logger.info(f"unzipped {doc_path} to {output_dir}") + + +def normalize_document(xml_content: str) -> str: + """ + Normalizes the XML document by removing extra spaces, preserving line breaks, + and removing hyperlinks while preserving text content. + + Args: + xml_content (str): The XML content to be normalized. + + Returns: + str: The normalized XML content. + """ + # Parse the XML content + parser = etree.XMLParser(ns_clean=True) + root = etree.fromstring(xml_content.encode("utf-8"), parser) + + # Extract namespaces + namespaces = {k: v for k, v in root.nsmap.items() if k} + + # Remove hyperlinks but preserve the text content + for hyperlink in root.xpath("//w:hyperlink", namespaces=namespaces): + parent = hyperlink.getparent() + index = list(parent).index(hyperlink) + + # Move all text-containing children (e.g., w:r elements) outside the hyperlink tag + for child in hyperlink: + parent.insert(index, child) + index += 1 + parent.remove(hyperlink) # Remove the element itself + + # Process each paragraph + for wp in root.xpath("//w:p", namespaces=namespaces): + first = True + + # Find all w:r elements containing w:t elements + for wr in wp.xpath(".//w:r", namespaces=namespaces): + wt = wr.find(".//w:t", namespaces) + if wt is not None and wt.text: + # Normalize spaces within the text, preserving new line breaks + wt.text = re.sub(r"[^\S\r\n]+", " ", wt.text) + + # Add a leading space if not the first fragment in the paragraph + if not first: + wt.text = " " + wt.text.lstrip() + else: + wt.text = wt.text.lstrip() + first = False + + # Remove trailing spaces from all fragments + wt.text = wt.text.rstrip() + + # Set the xml:space attribute to preserve + wt.set( + "{http://www.w3.org/XML/1998/namespace}space", + "preserve", + ) + + # Check if the text is empty after normalization + if not wt.text or wt.text.strip() == "": + # Remove the w:r element from its parent + wr.getparent().remove(wr) + + # Write back the XML content to a string + xml_str = etree.tostring(root, encoding="unicode", pretty_print=True) + + return xml_str + + +def replace_text_in_xml( + paragraphs: list[XMLParagraphWithParagraphPrediction], base_dir: str +) -> None: + tokens = pd.concat( + [gen_alignment_table(paragraph) for paragraph in paragraphs], + ignore_index=True, + ) + + fragments = ( + tokens.groupby(["xml_file", "paragraph_index", "fragment_index"]) + .agg({"target": " ".join, "start_char": "min", "end_char": "max"}) + .reset_index() + ) + + for xml_file, group in fragments.groupby("xml_file"): + group = group.sort_values("end_char", ascending=False) + + with open(f"{base_dir}/word/{xml_file}", "r+") as file: + content = file.read() + + for _, r in group.iterrows(): + start_char = r["start_char"] + end_char = r["end_char"] + + target = r["target"] + target = re.sub(r"[^\S\r\n]+", " ", target) + + content = content[:start_char] + target + content[end_char:] + + # MUST be at the end to dont screw up the indexes + content = normalize_document(content) + + file.seek(0) + file.write(content) + file.truncate() + + +def add_files_to_zip(zip_file: zipfile.ZipFile, directory: str) -> None: + """ + Adds all files in the specified directory to a zip file. + + Args: + zip_file (zipfile.ZipFile): The zip file to add the files to. + directory (str): The directory containing the files to be added. + """ + for root, _, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + zip_file.write(file_path, os.path.relpath(file_path, directory)) + + +def create_docx(xml_directory, output_file) -> None: + """ + Creates a new DOCX file by adding XML components from the specified directory. + + Args: + xml_directory (str): The directory containing the XML components. + output_file (str): The path to the output DOCX file. + """ + # Create a new zip file + with zipfile.ZipFile(output_file, "w") as docx: + # Add XML components + add_files_to_zip(docx, xml_directory) From 5ee8f3e5e5cf264469e0f9204a71059c2b5a46df Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Thu, 22 May 2025 15:37:08 +0000 Subject: [PATCH 06/11] =?UTF-8?q?=E2=9C=A8=20refactor:=20Simplify=20label?= =?UTF-8?q?=20merging=20logic=20in=20merge=5Fconsecutive=5Flabels=20functi?= =?UTF-8?q?on=20=F0=9F=94=A7=20fix:=20Clean=20up=20file=20opening=20syntax?= =?UTF-8?q?=20in=20load=5Fpickle=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aymurai/text/anonymization/alignment.py | 143 +-- aymurai/utils/pickle_data.py | 2 +- pyproject.toml | 1 + uv.lock | 1413 ++++++++++++----------- 4 files changed, 772 insertions(+), 787 deletions(-) diff --git a/aymurai/text/anonymization/alignment.py b/aymurai/text/anonymization/alignment.py index 5cf14a8..ff1cdb5 100644 --- a/aymurai/text/anonymization/alignment.py +++ b/aymurai/text/anonymization/alignment.py @@ -29,124 +29,43 @@ def merge_consecutive_labels(text: str, labels: list[DocLabel]) -> list[DocLabel """ Merge consecutive labels in the paragraph prediction. Args: - paragraph (XMLParagraphWithParagraphPrediction): A dictionary representing the prediction. + text (str): The original text. labels (list[DocLabel]): A list of labels to merge. Returns: - list[DocLabel]: A list of dictionaries representing the joined labels. + list[DocLabel]: A list of merged DocLabel objects. """ + if not labels: + return [] - # Reorder labels based on start indices labels = sorted(labels, key=lambda x: x.start_char) - - # merge_labels = [] - # pivot_label_attrs = None - - # # Iterate over labels - # for label in labels: - # # Get attributes - # label_text = label.attrs.aymurai_alt_text or label.text - # label_start_char = label.attrs.aymurai_alt_start_char or label.start_char - # label_end_char = label.attrs.aymurai_alt_end_char or label.end_char - # label_aymurai_label = label.attrs.aymurai_label - - # # Start a new group with the current label - # if pivot_label_attrs is None: - # pivot_label_attrs = label.attrs - # pivot_label_attrs.text = label_text - # pivot_label_attrs.start_char = label_start_char - # pivot_label_attrs.end_char = label_end_char - # pivot_label_attrs.aymurai_label = label_aymurai_label - - # # Extend the current group with the current label - # elif ( - # pivot_label_attrs.attrs.aymurai_label == label_aymurai_label - # and (label_start_char - pivot_label_attrs.end_char) <= 1 - # ): - # pivot_label_attrs.end_char = label_end_char - - # # Finish the current group and start a new one - # else: - # pivot_label_attrs.text = text[ - # pivot_label_attrs.start_char : pivot_label_attrs.end_char + 1 - # ] - # merge_labels.append(pivot_label_attrs) - - # pivot_label_attrs = label - # pivot_label_attrs.text = label_text - # pivot_label_attrs.start_char = label_start_char - # pivot_label_attrs.end_char = label_end_char - # pivot_label_attrs.attrs.aymurai_label = label_aymurai_label - - # # Finish the last group - # if pivot_label_attrs is not None: - # pivot_label_attrs.text = text[ - # pivot_label_attrs.start_char : pivot_label_attrs.end_char + 1 - # ] - # merge_labels.append(pivot_label_attrs) - - unified_labels = [] - current_group = None - - # Iterate over labels - for label in labels: - label = label.model_dump() - # Get attributes - label_text = label["attrs"]["aymurai_alt_text"] or label["text"] - start_char = label["attrs"]["aymurai_alt_start_char"] or label["start_char"] - end_char = label["attrs"]["aymurai_alt_end_char"] or label["end_char"] - aymurai_label = label["attrs"]["aymurai_label"] - - if current_group is None: - # Start a new group with the current label - current_group = { - "text": label_text, - "start_char": start_char, - "end_char": end_char, - "aymurai_label": aymurai_label, - } - elif ( - current_group["aymurai_label"] == aymurai_label - and (start_char - current_group["end_char"]) <= 1 - ): - # Extend the current group with the current label - current_group["end_char"] = end_char + merged = [] + current = labels[0] + + for label in labels[1:]: + # Get label info + cur_label = current.attrs.aymurai_label + next_label = label.attrs.aymurai_label + cur_end = current.attrs.aymurai_alt_end_char or current.end_char + next_start = label.attrs.aymurai_alt_start_char or label.start_char + # If same label and adjacent or overlapping + if cur_label == next_label and (next_start - cur_end) <= 1: + # Extend current label + current = DocLabel( + text=text[current.start_char : (label.end_char + 1)], + start_char=current.start_char, + end_char=label.end_char, + attrs=EntityAttributes( + aymurai_label=cur_label, + aymurai_alt_text=text[current.start_char : (label.end_char + 1)], + aymurai_alt_start_char=current.start_char, + aymurai_alt_end_char=label.end_char, + ), + ) else: - # Finish the current group and start a new one - current_group["text"] = text[ - current_group["start_char"] : current_group["end_char"] + 1 - ] - unified_labels.append(current_group) - current_group = { - "text": label_text, - "start_char": start_char, - "end_char": end_char, - "aymurai_label": aymurai_label, - } - - # Finish the last group - if current_group is not None: - current_group["text"] = text[ - current_group["start_char"] : current_group["end_char"] + 1 - ] - unified_labels.append(current_group) - - # return unified_labels - return [ - DocLabel( - text=label["text"], - start_char=label["start_char"], - end_char=label["end_char"], - attrs=EntityAttributes( - aymurai_label=label["aymurai_label"], - aymurai_alt_text=label["text"], - aymurai_alt_start_char=label["start_char"], - aymurai_alt_end_char=label["end_char"], - ), - ) - for label in unified_labels - ] - - # return merge_labels + merged.append(current) + current = label + merged.append(current) + return merged def replace_labels_in_text(text: str, prediction: PredictionPublic | None) -> str: diff --git a/aymurai/utils/pickle_data.py b/aymurai/utils/pickle_data.py index 4d5ceeb..d1d8caf 100644 --- a/aymurai/utils/pickle_data.py +++ b/aymurai/utils/pickle_data.py @@ -9,6 +9,6 @@ def save_pickle(object_: Any, output_path: str): def load_pickle(input_path: str) -> Any: - with (open(input_path, "rb")) as f: + with open(input_path, "rb") as f: object_ = pickle.load(f) return object_ diff --git a/pyproject.toml b/pyproject.toml index 4485021..3bee606 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,7 @@ dev = [ "nbstripout>=0.8.0", "jupyter>=1.1.1", "pip>=24.3.1", + "ruff>=0.11.9", ] [tool.setuptools.packages.find] diff --git a/uv.lock b/uv.lock index 66a8332..12b9f1f 100644 --- a/uv.lock +++ b/uv.lock @@ -3,11 +3,11 @@ requires-python = "==3.10.*" [[package]] name = "absl-py" -version = "2.1.0" +version = "2.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/8f/fc001b92ecc467cc32ab38398bd0bfb45df46e7523bf33c2ad22a505f06e/absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff", size = 118055 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f0/e6342091061ed3a46aadc116b13edd7bb5249c3ab1b3ef07f24b0c248fc3/absl_py-2.2.2.tar.gz", hash = "sha256:bf25b2c2eed013ca456918c453d687eab4e8309fba81ee2f4c1a6aa2494175eb", size = 119982 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/ad/e0d3c824784ff121c03cc031f944bc7e139a8f1870ffd2845cc2dd76f6c4/absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308", size = 133706 }, + { url = "https://files.pythonhosted.org/packages/f6/d4/349f7f4bd5ea92dab34f5bb0fe31775ef6c311427a14d5a5b31ecb442341/absl_py-2.2.2-py3-none-any.whl", hash = "sha256:e5797bc6abe45f64fd95dc06394ca3f2bedf3b5d895e9da691c9ee3397d70092", size = 135565 }, ] [[package]] @@ -30,16 +30,16 @@ wheels = [ [[package]] name = "aiohappyeyeballs" -version = "2.4.4" +version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, ] [[package]] name = "aiohttp" -version = "3.11.11" +version = "3.11.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -51,23 +51,24 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } +sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653 } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/7d/ff2e314b8f9e0b1df833e2d4778eaf23eae6b8cc8f922495d110ddcbf9e1/aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8", size = 708550 }, - { url = "https://files.pythonhosted.org/packages/09/b8/aeb4975d5bba233d6f246941f5957a5ad4e3def8b0855a72742e391925f2/aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5", size = 468430 }, - { url = "https://files.pythonhosted.org/packages/9c/5b/5b620279b3df46e597008b09fa1e10027a39467387c2332657288e25811a/aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2", size = 455593 }, - { url = "https://files.pythonhosted.org/packages/d8/75/0cdf014b816867d86c0bc26f3d3e3f194198dbf33037890beed629cd4f8f/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43", size = 1584635 }, - { url = "https://files.pythonhosted.org/packages/df/2f/95b8f4e4dfeb57c1d9ad9fa911ede35a0249d75aa339edd2c2270dc539da/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f", size = 1632363 }, - { url = "https://files.pythonhosted.org/packages/39/cb/70cf69ea7c50f5b0021a84f4c59c3622b2b3b81695f48a2f0e42ef7eba6e/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d", size = 1668315 }, - { url = "https://files.pythonhosted.org/packages/2f/cc/3a3fc7a290eabc59839a7e15289cd48f33dd9337d06e301064e1e7fb26c5/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef", size = 1589546 }, - { url = "https://files.pythonhosted.org/packages/15/b4/0f7b0ed41ac6000e283e7332f0f608d734b675a8509763ca78e93714cfb0/aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438", size = 1544581 }, - { url = "https://files.pythonhosted.org/packages/58/b9/4d06470fd85c687b6b0e31935ef73dde6e31767c9576d617309a2206556f/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3", size = 1529256 }, - { url = "https://files.pythonhosted.org/packages/61/a2/6958b1b880fc017fd35f5dfb2c26a9a50c755b75fd9ae001dc2236a4fb79/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55", size = 1536592 }, - { url = "https://files.pythonhosted.org/packages/0f/dd/b974012a9551fd654f5bb95a6dd3f03d6e6472a17e1a8216dd42e9638d6c/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e", size = 1607446 }, - { url = "https://files.pythonhosted.org/packages/e0/d3/6c98fd87e638e51f074a3f2061e81fcb92123bcaf1439ac1b4a896446e40/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33", size = 1628809 }, - { url = "https://files.pythonhosted.org/packages/a8/2e/86e6f85cbca02be042c268c3d93e7f35977a0e127de56e319bdd1569eaa8/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c", size = 1564291 }, - { url = "https://files.pythonhosted.org/packages/0b/8d/1f4ef3503b767717f65e1f5178b0173ab03cba1a19997ebf7b052161189f/aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745", size = 416601 }, - { url = "https://files.pythonhosted.org/packages/ad/86/81cb83691b5ace3d9aa148dc42bacc3450d749fc88c5ec1973573c1c1779/aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9", size = 442007 }, + { url = "https://files.pythonhosted.org/packages/c7/c3/e5f64af7e97a02f547020e6ff861595766bb5ecb37c7492fac9fe3c14f6c/aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4", size = 711703 }, + { url = "https://files.pythonhosted.org/packages/5f/2f/53c26e96efa5fd01ebcfe1fefdfb7811f482bb21f4fa103d85eca4dcf888/aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6", size = 471348 }, + { url = "https://files.pythonhosted.org/packages/80/47/dcc248464c9b101532ee7d254a46f6ed2c1fd3f4f0f794cf1f2358c0d45b/aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609", size = 457611 }, + { url = "https://files.pythonhosted.org/packages/4c/ca/67d816ef075e8ac834b5f1f6b18e8db7d170f7aebaf76f1be462ea10cab0/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55", size = 1591976 }, + { url = "https://files.pythonhosted.org/packages/46/00/0c120287aa51c744438d99e9aae9f8c55ca5b9911c42706966c91c9d68d6/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f", size = 1632819 }, + { url = "https://files.pythonhosted.org/packages/54/a3/3923c9040cd4927dfee1aa017513701e35adcfc35d10729909688ecaa465/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94", size = 1666567 }, + { url = "https://files.pythonhosted.org/packages/e0/ab/40dacb15c0c58f7f17686ea67bc186e9f207341691bdb777d1d5ff4671d5/aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1", size = 1594959 }, + { url = "https://files.pythonhosted.org/packages/0d/98/d40c2b7c4a5483f9a16ef0adffce279ced3cc44522e84b6ba9e906be5168/aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415", size = 1538516 }, + { url = "https://files.pythonhosted.org/packages/cf/10/e0bf3a03524faac45a710daa034e6f1878b24a1fef9c968ac8eb786ae657/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7", size = 1529037 }, + { url = "https://files.pythonhosted.org/packages/ad/d6/5ff5282e00e4eb59c857844984cbc5628f933e2320792e19f93aff518f52/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb", size = 1546813 }, + { url = "https://files.pythonhosted.org/packages/de/96/f1014f84101f9b9ad2d8acf3cc501426475f7f0cc62308ae5253e2fac9a7/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d", size = 1523852 }, + { url = "https://files.pythonhosted.org/packages/a5/86/ec772c6838dd6bae3229065af671891496ac1834b252f305cee8152584b2/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421", size = 1603766 }, + { url = "https://files.pythonhosted.org/packages/84/38/31f85459c9402d409c1499284fc37a96f69afadce3cfac6a1b5ab048cbf1/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643", size = 1620647 }, + { url = "https://files.pythonhosted.org/packages/31/2f/54aba0040764dd3d362fb37bd6aae9b3034fcae0b27f51b8a34864e48209/aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868", size = 1559260 }, + { url = "https://files.pythonhosted.org/packages/ca/d2/a05c7dd9e1b6948c1c5d04f1a8bcfd7e131923fa809bb87477d5c76f1517/aiohttp-3.11.18-cp310-cp310-win32.whl", hash = "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f", size = 418051 }, + { url = "https://files.pythonhosted.org/packages/39/e2/796a6179e8abe267dfc84614a50291560a989d28acacbc5dab3bcd4cbec4/aiohttp-3.11.18-cp310-cp310-win_amd64.whl", hash = "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9", size = 442908 }, ] [[package]] @@ -84,16 +85,16 @@ wheels = [ [[package]] name = "alembic" -version = "1.14.1" +version = "1.15.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/09/f844822e4e847a3f0bd41797f93c4674cd4d2462a3f6c459aa528cdf786e/alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213", size = 1918219 } +sdist = { url = "https://files.pythonhosted.org/packages/e6/57/e314c31b261d1e8a5a5f1908065b4ff98270a778ce7579bd4254477209a7/alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7", size = 1925573 } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/ac0991d1745f7d755fc1cd381b3990a45b404b4d008fc75e2a983516fbfe/alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", size = 233565 }, + { url = "https://files.pythonhosted.org/packages/41/18/d89a443ed1ab9bcda16264716f809c663866d4ca8de218aa78fd50b38ead/alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53", size = 231911 }, ] [[package]] @@ -107,7 +108,7 @@ wheels = [ [[package]] name = "anyio" -version = "4.8.0" +version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup" }, @@ -115,9 +116,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, ] [[package]] @@ -208,14 +209,14 @@ wheels = [ [[package]] name = "async-lru" -version = "2.0.4" +version = "2.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/e2/2b4651eff771f6fd900d233e175ddc5e2be502c7eb62c0c42f975c6d36cd/async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627", size = 10019 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/9f/3c3503693386c4b0f245eaf5ca6198e3b28879ca0a40bde6b0e319793453/async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224", size = 6111 }, + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069 }, ] [[package]] @@ -229,16 +230,16 @@ wheels = [ [[package]] name = "attrs" -version = "25.1.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] [[package]] name = "aymurai" -version = "1.1.1.dev39+g447815e.d20250215" +version = "1.1.7rc2.dev12+gedb4ea0.d20250512" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -289,6 +290,7 @@ dev = [ { name = "plotly" }, { name = "pre-commit" }, { name = "rich" }, + { name = "ruff" }, { name = "seaborn" }, ] @@ -342,16 +344,17 @@ dev = [ { name = "plotly", specifier = ">=5.24.1" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "rich", specifier = ">=13.9.4" }, + { name = "ruff", specifier = ">=0.11.9" }, { name = "seaborn", specifier = ">=0.13.2" }, ] [[package]] name = "babel" -version = "2.16.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] [[package]] @@ -401,48 +404,48 @@ css = [ [[package]] name = "boto3" -version = "1.36.6" +version = "1.38.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/31/f6189fcb81156cd2e7f7616e4c95958a47e53c12253c5e86a9dcc1a529c1/boto3-1.36.6.tar.gz", hash = "sha256:b36feae061dc0793cf311468956a0a9e99215ce38bc99a1a4e55a5b105f16297", size = 110998 } +sdist = { url = "https://files.pythonhosted.org/packages/b4/76/69b7ac6613bafb355edba21045372cb8dc6ff5af3252efe6f30855d3fb20/boto3-1.38.4.tar.gz", hash = "sha256:4990df0087fe7be944ba06c2d1e6512b5a24f821af5a4881f24309e13ae29e68", size = 111763 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/44/4b4d7579297708b84d846a223b6e3be93767d12a3ee997e162e2a3a371a5/boto3-1.36.6-py3-none-any.whl", hash = "sha256:6d473f0f340d02b4e9ad5b8e68786a09728101a8b950231b89ebdaf72b6dca21", size = 139166 }, + { url = "https://files.pythonhosted.org/packages/8c/74/11971c651e88169ffdba829e106dadadb64480748c3c5ecc08e55d71fc13/boto3-1.38.4-py3-none-any.whl", hash = "sha256:ab315d38409f5b3262b653a10b0fac786bcff7e51e03dcb99ff38ba16bf85630", size = 139899 }, ] [[package]] name = "botocore" -version = "1.36.6" +version = "1.38.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b6/bd1a28becf386a70ca41aa6b76b0d65d03aed81d39fac662d1f97754ffca/botocore-1.36.6.tar.gz", hash = "sha256:4864c53d638da191a34daf3ede3ff1371a3719d952cc0c6bd24ce2836a38dd77", size = 13479626 } +sdist = { url = "https://files.pythonhosted.org/packages/85/a2/5e866edcf30c7b5703dc345355a0314d1e4f731762d57aabd9665df70de7/botocore-1.38.4.tar.gz", hash = "sha256:6143546bb56f1da4dff8d285cb6a3b8b0b6442451fe5937cb48a62bf7275386f", size = 13856647 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/ef/a0ac0d749c82fbaba28aa7b87a8ef5760de744e57dcbbfe3b99a0d65cf87/botocore-1.36.6-py3-none-any.whl", hash = "sha256:f77bbbb03fb420e260174650fb5c0cc142ec20a96967734eed2b0ef24334ef34", size = 13308005 }, + { url = "https://files.pythonhosted.org/packages/6b/67/93837ce62b4e49eaf77f076c4781e942386acb70ad1923429df8c6b1371e/botocore-1.38.4-py3-none-any.whl", hash = "sha256:6206cf07be1069efaead2ddc858eb752dafef276ebbe88ac32b5c427b1d90570", size = 13516497 }, ] [[package]] name = "cachetools" -version = "5.5.1" +version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, ] [[package]] @@ -543,9 +546,12 @@ wheels = [ [[package]] name = "compressed-rtf" -version = "1.0.6" +version = "1.0.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ac/abb196bb0b42a239d605fe97c314c3312374749013a07da4e6e0408f223c/compressed_rtf-1.0.6.tar.gz", hash = "sha256:c1c827f1d124d24608981a56e8b8691eb1f2a69a78ccad6440e7d92fde1781dd", size = 5800 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/0c/929a4e8ef9d7143f54d77dadb5f370cc7b98534b1bd6e1124d0abe8efb24/compressed_rtf-1.0.7.tar.gz", hash = "sha256:7c30859334839f3cdc7d10796af5b434bb326b9df7cb5a65e95a8eacb2951b0e", size = 8152 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/1d/62f5bf92e12335eb63517f42671ed78512d48bbc69e02a942dd7b90f03f0/compressed_rtf-1.0.7-py3-none-any.whl", hash = "sha256:b7904921d78c67a0a4b7fff9fb361a00ae2b447b6edca010ce321cd98fa0fcc0", size = 7968 }, +] [[package]] name = "conllu" @@ -558,26 +564,26 @@ wheels = [ [[package]] name = "contourpy" -version = "1.3.1" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466 }, - { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314 }, - { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003 }, - { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896 }, - { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814 }, - { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969 }, - { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162 }, - { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328 }, - { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861 }, - { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605 }, - { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040 }, - { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221 }, + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, ] [[package]] @@ -591,7 +597,7 @@ wheels = [ [[package]] name = "datasets" -version = "3.2.0" +version = "3.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -609,9 +615,9 @@ dependencies = [ { name = "tqdm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/48/744286c044e2b942d4fa67f92816126522ad1f0675def0ea3264e6242005/datasets-3.2.0.tar.gz", hash = "sha256:9a6e1a356052866b5dbdd9c9eedb000bf3fc43d986e3584d9b028f4976937229", size = 558366 } +sdist = { url = "https://files.pythonhosted.org/packages/10/af/8c1d10daf37383c32ab0a7461eaa4d5c7a3c47808fe5a8563744de002df7/datasets-3.5.1.tar.gz", hash = "sha256:f835b45dbbd7065c1191734b6f7c8d96fdf8c5751feaa5aa52b2a0dc43eea58f", size = 568915 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/84/0df6c5981f5fc722381662ff8cfbdf8aad64bec875f75d80b55bfef394ce/datasets-3.2.0-py3-none-any.whl", hash = "sha256:f3d2ba2698b7284a4518019658596a6a8bc79f31e51516524249d6c59cf0fe2a", size = 480647 }, + { url = "https://files.pythonhosted.org/packages/e3/f5/668b3444a2f487b0052b908af631fe39eeb2bdb2359d9bbc2c3b80b71119/datasets-3.5.1-py3-none-any.whl", hash = "sha256:4074dda8dd6e9ece242b1580a8ef3928777d59ae1db144d911229e443a093cbb", size = 491436 }, ] [[package]] @@ -621,24 +627,24 @@ source = { git = "https://github.com/jedzill4/datetime_matcher#0e5793e8d1e3653f7 [[package]] name = "debugpy" -version = "1.8.12" +version = "1.8.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/25/c74e337134edf55c4dfc9af579eccb45af2393c40960e2795a94351e8140/debugpy-1.8.12.tar.gz", hash = "sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce", size = 1641122 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444 } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/19/dd58334c0a1ec07babf80bf29fb8daf1a7ca4c1a3bbe61548e40616ac087/debugpy-1.8.12-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:a2ba7ffe58efeae5b8fad1165357edfe01464f9aef25e814e891ec690e7dd82a", size = 2076091 }, - { url = "https://files.pythonhosted.org/packages/4c/37/bde1737da15f9617d11ab7b8d5267165f1b7dae116b2585a6643e89e1fa2/debugpy-1.8.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbbd4149c4fc5e7d508ece083e78c17442ee13b0e69bfa6bd63003e486770f45", size = 3560717 }, - { url = "https://files.pythonhosted.org/packages/d9/ca/bc67f5a36a7de072908bc9e1156c0f0b272a9a2224cf21540ab1ffd71a1f/debugpy-1.8.12-cp310-cp310-win32.whl", hash = "sha256:b202f591204023b3ce62ff9a47baa555dc00bb092219abf5caf0e3718ac20e7c", size = 5180672 }, - { url = "https://files.pythonhosted.org/packages/c1/b9/e899c0a80dfa674dbc992f36f2b1453cd1ee879143cdb455bc04fce999da/debugpy-1.8.12-cp310-cp310-win_amd64.whl", hash = "sha256:9649eced17a98ce816756ce50433b2dd85dfa7bc92ceb60579d68c053f98dff9", size = 5212702 }, - { url = "https://files.pythonhosted.org/packages/38/c4/5120ad36405c3008f451f94b8f92ef1805b1e516f6ff870f331ccb3c4cc0/debugpy-1.8.12-py2.py3-none-any.whl", hash = "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6", size = 5229490 }, + { url = "https://files.pythonhosted.org/packages/fc/df/156df75a41aaebd97cee9d3870fe68f8001b6c1c4ca023e221cfce69bece/debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339", size = 2076510 }, + { url = "https://files.pythonhosted.org/packages/69/cd/4fc391607bca0996db5f3658762106e3d2427beaef9bfd363fd370a3c054/debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79", size = 3559614 }, + { url = "https://files.pythonhosted.org/packages/1a/42/4e6d2b9d63e002db79edfd0cb5656f1c403958915e0e73ab3e9220012eec/debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987", size = 5208588 }, + { url = "https://files.pythonhosted.org/packages/97/b1/cc9e4e5faadc9d00df1a64a3c2d5c5f4b9df28196c39ada06361c5141f89/debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84", size = 5241043 }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230 }, ] [[package]] name = "decorator" -version = "5.1.1" +version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, ] [[package]] @@ -652,14 +658,14 @@ wheels = [ [[package]] name = "deprecated" -version = "1.2.17" +version = "1.2.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/20/caa25c084ebad492360bf28ba5cb74f27b50fc6f3df965fd0add2b5b5993/deprecated-1.2.17.tar.gz", hash = "sha256:0114a10f0bbb750b90b2c2296c90cf7e9eaeb0abb5cf06c80de2c60138de0a82", size = 2928237 } +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/90/89be9a665bf63d3baaba6f34d9a514655abb42be2cc129f31c96df2cef51/Deprecated-1.2.17-py2.py3-none-any.whl", hash = "sha256:69cdc0a751671183f569495e2efb14baee4344b0236342eec29f1fde25d61818", size = 9140 }, + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, ] [[package]] @@ -706,9 +712,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf57 [[package]] name = "docx2txt" -version = "0.8" +version = "0.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/7d/60ee3f2b16d9bfdfa72e8599470a2c1a5b759cb113c6fe1006be28359327/docx2txt-0.8.tar.gz", hash = "sha256:2c06d98d7cfe2d3947e5760a57d924e3ff07745b379c8737723922e7009236e5", size = 2814 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/07/4486a038624e885e227fe79111914c01f55aa70a51920ff1a7f2bd216d10/docx2txt-0.9.tar.gz", hash = "sha256:18013f6229b14909028b19aa7bf4f8f3d6e4632d7b089ab29f7f0a4d1f660e28", size = 3613 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/51/756e71bec48ece0ecc2a10e921ef2756e197dcb7e478f2b43673b6683902/docx2txt-0.9-py3-none-any.whl", hash = "sha256:e3718c0653fd6f2fcf4b51b02a61452ad1c38a4c163bcf0a6fd9486cd38f529a", size = 4025 }, +] [[package]] name = "ebcdic" @@ -779,16 +788,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.7" +version = "0.115.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/f5/3f921e59f189e513adb9aef826e2841672d50a399fead4e69afdeb808ff4/fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015", size = 293177 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/7f/bbd4dcf0faf61bc68a01939256e2ed02d681e9334c1a3cef24d5f77aba9f/fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e", size = 94777 }, + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, ] [package.optional-dependencies] @@ -831,11 +840,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.17.0" +version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, ] [[package]] @@ -886,28 +895,28 @@ wheels = [ [[package]] name = "flatbuffers" -version = "25.1.24" +version = "25.2.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/20/c380c311843318b577650286b2c7eaaac3a011fb982df0050bdbd7e453c5/flatbuffers-25.1.24.tar.gz", hash = "sha256:e0f7b7d806c0abdf166275492663130af40c11f89445045fbef0aa3c9a8643ad", size = 22155 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/e2/b066e6e02d67bf5261a6d7539648c6da3365cc9eff3eb6d82009595d84d9/flatbuffers-25.1.24-py2.py3-none-any.whl", hash = "sha256:1abfebaf4083117225d0723087ea909896a34e3fec933beedb490d595ba24145", size = 30955 }, + { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953 }, ] [[package]] name = "fonttools" -version = "4.55.6" +version = "4.57.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2e/0b11e907b90665253dbad425479e874e38a9e81ced397a4e3312b9116935/fonttools-4.55.6.tar.gz", hash = "sha256:1beb4647a0df5ceaea48015656525eb8081af226fe96554089fd3b274d239ef0", size = 3500677 } +sdist = { url = "https://files.pythonhosted.org/packages/03/2d/a9a0b6e3a0cf6bd502e64fc16d894269011930cabfc89aee20d1635b1441/fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de", size = 3492448 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/05/8c1a6ab8c443525c1cedee94d0371ec45cbcd11e4b01328ff10dcc483134/fonttools-4.55.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:57d55fc965e5dd20c8a60d880e0f43bafb506be87af0b650bdc42591e41e0d0d", size = 2774906 }, - { url = "https://files.pythonhosted.org/packages/cd/53/de15cea829b49d50a0d0942d75bc71ca680536265abed03ce873ae71787b/fonttools-4.55.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:127999618afe3a2490fad54bab0650c5fbeab1f8109bdc0205f6ad34306deb8b", size = 2303345 }, - { url = "https://files.pythonhosted.org/packages/d2/23/90159149cc907ea2da0ca7b7baf5ad783c902de219ecb28bca3f789b82c3/fonttools-4.55.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3226d40cb92787e09dcc3730f54b3779dfe56bdfea624e263685ba17a6faac4", size = 4584790 }, - { url = "https://files.pythonhosted.org/packages/26/db/8d33a4575efe7ecd0487d4a53369d086ab7d879069e4c62d3687dec53941/fonttools-4.55.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e82772f70b84e17aa36e9f236feb2a4f73cb686ec1e162557a36cf759d1acd58", size = 4627464 }, - { url = "https://files.pythonhosted.org/packages/a3/c9/e90342b5eebce21ba2b04ce879c66e0316a5faaa7337498dfb5032953055/fonttools-4.55.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a632f85bd73e002b771bcbcdc512038fa5d2e09bb18c03a22fb8d400ea492ddf", size = 4581741 }, - { url = "https://files.pythonhosted.org/packages/e1/98/f4297b65849f15f6b49349a1ffc21e93d1859fa3579d0d5cb1590317060c/fonttools-4.55.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:791e0cf862cdd3a252df395f1bb5f65e3a760f1da3c7ce184d0f7998c266614d", size = 4751195 }, - { url = "https://files.pythonhosted.org/packages/63/73/9dcf4040edbdc5e33cc23d2b7d6a6d4bb50605efd5686d6b782815f4a818/fonttools-4.55.6-cp310-cp310-win32.whl", hash = "sha256:94f7f2c5c5f3a6422e954ecb6d37cc363e27d6f94050a7ed3f79f12157af6bb2", size = 2178422 }, - { url = "https://files.pythonhosted.org/packages/45/f0/ec0ce63f910db60a566201a550c06205595c10c980f6c74885f53cdf512b/fonttools-4.55.6-cp310-cp310-win_amd64.whl", hash = "sha256:2d15e02b93a46982a8513a208e8f89148bca8297640527365625be56151687d0", size = 2222886 }, - { url = "https://files.pythonhosted.org/packages/1e/6a/6afc55d75036b8d3fe5ceaea2e8da2c04e8f3b298325de73a35f098cb9a8/fonttools-4.55.6-py3-none-any.whl", hash = "sha256:d20ab5a78d0536c26628eaadba661e7ae2427b1e5c748a0a510a44d914e1b155", size = 1112524 }, + { url = "https://files.pythonhosted.org/packages/db/17/3ddfd1881878b3f856065130bb603f5922e81ae8a4eb53bce0ea78f765a8/fonttools-4.57.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:babe8d1eb059a53e560e7bf29f8e8f4accc8b6cfb9b5fd10e485bde77e71ef41", size = 2756260 }, + { url = "https://files.pythonhosted.org/packages/26/2b/6957890c52c030b0bf9e0add53e5badab4682c6ff024fac9a332bb2ae063/fonttools-4.57.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81aa97669cd726349eb7bd43ca540cf418b279ee3caba5e2e295fb4e8f841c02", size = 2284691 }, + { url = "https://files.pythonhosted.org/packages/cc/8e/c043b4081774e5eb06a834cedfdb7d432b4935bc8c4acf27207bdc34dfc4/fonttools-4.57.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0e9618630edd1910ad4f07f60d77c184b2f572c8ee43305ea3265675cbbfe7e", size = 4566077 }, + { url = "https://files.pythonhosted.org/packages/59/bc/e16ae5d9eee6c70830ce11d1e0b23d6018ddfeb28025fda092cae7889c8b/fonttools-4.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34687a5d21f1d688d7d8d416cb4c5b9c87fca8a1797ec0d74b9fdebfa55c09ab", size = 4608729 }, + { url = "https://files.pythonhosted.org/packages/25/13/e557bf10bb38e4e4c436d3a9627aadf691bc7392ae460910447fda5fad2b/fonttools-4.57.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69ab81b66ebaa8d430ba56c7a5f9abe0183afefd3a2d6e483060343398b13fb1", size = 4759646 }, + { url = "https://files.pythonhosted.org/packages/bc/c9/5e2952214d4a8e31026bf80beb18187199b7001e60e99a6ce19773249124/fonttools-4.57.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d639397de852f2ccfb3134b152c741406752640a266d9c1365b0f23d7b88077f", size = 4941652 }, + { url = "https://files.pythonhosted.org/packages/df/04/e80242b3d9ec91a1f785d949edc277a13ecfdcfae744de4b170df9ed77d8/fonttools-4.57.0-cp310-cp310-win32.whl", hash = "sha256:cc066cb98b912f525ae901a24cd381a656f024f76203bc85f78fcc9e66ae5aec", size = 2159432 }, + { url = "https://files.pythonhosted.org/packages/33/ba/e858cdca275daf16e03c0362aa43734ea71104c3b356b2100b98543dba1b/fonttools-4.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a64edd3ff6a7f711a15bd70b4458611fb240176ec11ad8845ccbab4fe6745db", size = 2203869 }, + { url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605 }, ] [[package]] @@ -921,35 +930,37 @@ wheels = [ [[package]] name = "frozenlist" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/79/29d44c4af36b2b240725dce566b20f63f9b36ef267aaaa64ee7466f4f2f8/frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", size = 94451 }, - { url = "https://files.pythonhosted.org/packages/47/47/0c999aeace6ead8a44441b4f4173e2261b18219e4ad1fe9a479871ca02fc/frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", size = 54301 }, - { url = "https://files.pythonhosted.org/packages/8d/60/107a38c1e54176d12e06e9d4b5d755b677d71d1219217cee063911b1384f/frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", size = 52213 }, - { url = "https://files.pythonhosted.org/packages/17/62/594a6829ac5679c25755362a9dc93486a8a45241394564309641425d3ff6/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", size = 240946 }, - { url = "https://files.pythonhosted.org/packages/7e/75/6c8419d8f92c80dd0ee3f63bdde2702ce6398b0ac8410ff459f9b6f2f9cb/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", size = 264608 }, - { url = "https://files.pythonhosted.org/packages/88/3e/82a6f0b84bc6fb7e0be240e52863c6d4ab6098cd62e4f5b972cd31e002e8/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", size = 261361 }, - { url = "https://files.pythonhosted.org/packages/fd/85/14e5f9ccac1b64ff2f10c927b3ffdf88772aea875882406f9ba0cec8ad84/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", size = 231649 }, - { url = "https://files.pythonhosted.org/packages/ee/59/928322800306f6529d1852323014ee9008551e9bb027cc38d276cbc0b0e7/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", size = 241853 }, - { url = "https://files.pythonhosted.org/packages/7d/bd/e01fa4f146a6f6c18c5d34cab8abdc4013774a26c4ff851128cd1bd3008e/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", size = 243652 }, - { url = "https://files.pythonhosted.org/packages/a5/bd/e4771fd18a8ec6757033f0fa903e447aecc3fbba54e3630397b61596acf0/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", size = 241734 }, - { url = "https://files.pythonhosted.org/packages/21/13/c83821fa5544af4f60c5d3a65d054af3213c26b14d3f5f48e43e5fb48556/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", size = 260959 }, - { url = "https://files.pythonhosted.org/packages/71/f3/1f91c9a9bf7ed0e8edcf52698d23f3c211d8d00291a53c9f115ceb977ab1/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", size = 262706 }, - { url = "https://files.pythonhosted.org/packages/4c/22/4a256fdf5d9bcb3ae32622c796ee5ff9451b3a13a68cfe3f68e2c95588ce/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", size = 250401 }, - { url = "https://files.pythonhosted.org/packages/af/89/c48ebe1f7991bd2be6d5f4ed202d94960c01b3017a03d6954dd5fa9ea1e8/frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", size = 45498 }, - { url = "https://files.pythonhosted.org/packages/28/2f/cc27d5f43e023d21fe5c19538e08894db3d7e081cbf582ad5ed366c24446/frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", size = 51622 }, - { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/03/22e4eb297981d48468c3d9982ab6076b10895106d3039302a943bb60fd70/frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e", size = 160584 }, + { url = "https://files.pythonhosted.org/packages/2b/b8/c213e35bcf1c20502c6fd491240b08cdd6ceec212ea54873f4cae99a51e4/frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352", size = 124099 }, + { url = "https://files.pythonhosted.org/packages/2b/33/df17b921c2e37b971407b4045deeca6f6de7caf0103c43958da5e1b85e40/frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b", size = 122106 }, + { url = "https://files.pythonhosted.org/packages/8e/09/93f0293e8a95c05eea7cf9277fef8929fb4d0a2234ad9394cd2a6b6a6bb4/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc", size = 287205 }, + { url = "https://files.pythonhosted.org/packages/5e/34/35612f6f1b1ae0f66a4058599687d8b39352ade8ed329df0890fb553ea1e/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869", size = 295079 }, + { url = "https://files.pythonhosted.org/packages/e5/ca/51577ef6cc4ec818aab94a0034ef37808d9017c2e53158fef8834dbb3a07/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106", size = 308068 }, + { url = "https://files.pythonhosted.org/packages/36/27/c63a23863b9dcbd064560f0fea41b516bbbf4d2e8e7eec3ff880a96f0224/frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24", size = 305640 }, + { url = "https://files.pythonhosted.org/packages/33/c2/91720b3562a6073ba604547a417c8d3bf5d33e4c8f1231f3f8ff6719e05c/frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd", size = 278509 }, + { url = "https://files.pythonhosted.org/packages/d0/6e/1b64671ab2fca1ebf32c5b500205724ac14c98b9bc1574b2ef55853f4d71/frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8", size = 287318 }, + { url = "https://files.pythonhosted.org/packages/66/30/589a8d8395d5ebe22a6b21262a4d32876df822c9a152e9f2919967bb8e1a/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c", size = 290923 }, + { url = "https://files.pythonhosted.org/packages/4d/e0/2bd0d2a4a7062b7e4b5aad621697cd3579e5d1c39d99f2833763d91e746d/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75", size = 304847 }, + { url = "https://files.pythonhosted.org/packages/70/a0/a1a44204398a4b308c3ee1b7bf3bf56b9dcbcc4e61c890e038721d1498db/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249", size = 285580 }, + { url = "https://files.pythonhosted.org/packages/78/ed/3862bc9abe05839a6a5f5bab8b6bbdf0fc9369505cb77cd15b8c8948f6a0/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769", size = 304033 }, + { url = "https://files.pythonhosted.org/packages/2c/9c/1c48454a9e1daf810aa6d977626c894b406651ca79d722fce0f13c7424f1/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02", size = 307566 }, + { url = "https://files.pythonhosted.org/packages/35/ef/cb43655c21f1bad5c42bcd540095bba6af78bf1e474b19367f6fd67d029d/frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3", size = 295354 }, + { url = "https://files.pythonhosted.org/packages/9f/59/d8069a688a0f54a968c73300d6013e4786b029bfec308664094130dcea66/frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812", size = 115586 }, + { url = "https://files.pythonhosted.org/packages/f9/a6/8f0cef021912ba7aa3b9920fe0a4557f6e85c41bbf71bb568cd744828df5/frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1", size = 120845 }, + { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404 }, ] [[package]] name = "fsspec" -version = "2024.9.0" +version = "2025.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/7c/12b0943011daaaa9c35c2a2e22e5eb929ac90002f08f1259d69aedad84de/fsspec-2024.9.0.tar.gz", hash = "sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8", size = 286206 } +sdist = { url = "https://files.pythonhosted.org/packages/34/f4/5721faf47b8c499e776bc34c6a8fc17efdf7fdef0b00f398128bc5dcb4ac/fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972", size = 298491 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/a0/6aaea0c2fbea2f89bfd5db25fb1e3481896a423002ebe4e55288907a97a3/fsspec-2024.9.0-py3-none-any.whl", hash = "sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b", size = 179253 }, + { url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615 }, ] [package.optional-dependencies] @@ -996,16 +1007,16 @@ wheels = [ [[package]] name = "google-auth" -version = "2.38.0" +version = "2.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/8e/8f45c9a32f73e786e954b8f9761c61422955d23c45d1e8c347f9b4b59e8e/google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7", size = 274834 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, + { url = "https://files.pythonhosted.org/packages/ce/12/ad37a1ef86006d0a0117fc06a4a00bd461c775356b534b425f00dde208ea/google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2", size = 212319 }, ] [[package]] @@ -1035,74 +1046,75 @@ wheels = [ [[package]] name = "greenlet" -version = "3.1.1" +version = "3.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/74/907bb43af91782e0366b0960af62a8ce1f9398e4291cac7beaeffbee0c04/greenlet-3.2.1.tar.gz", hash = "sha256:9f4dd4b4946b14bb3bf038f81e1d2e535b7d94f1b2a59fdba1293cd9c1a0a4d7", size = 184475 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, + { url = "https://files.pythonhosted.org/packages/df/3e/6332bb2d1e43ec6270e0b97bf253cd704691ee55e4e52196cb7da8f774e9/greenlet-3.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:777c1281aa7c786738683e302db0f55eb4b0077c20f1dc53db8852ffaea0a6b0", size = 267364 }, + { url = "https://files.pythonhosted.org/packages/73/c1/c47cc96878c4eda993a2deaba15af3cfdc87cf8e2e3c4c20726dea541a8c/greenlet-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3059c6f286b53ea4711745146ffe5a5c5ff801f62f6c56949446e0f6461f8157", size = 625721 }, + { url = "https://files.pythonhosted.org/packages/c8/65/df1ff1a505a62b08d31da498ddc0c9992e9c536c01944f8b800a7cf17ac6/greenlet-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1a40a17e2c7348f5eee5d8e1b4fa6a937f0587eba89411885a36a8e1fc29bd2", size = 636983 }, + { url = "https://files.pythonhosted.org/packages/e8/1d/29944dcaaf5e482f7bff617de15f29e17cc0e74c7393888f8a43d7f6229e/greenlet-3.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5193135b3a8d0017cb438de0d49e92bf2f6c1c770331d24aa7500866f4db4017", size = 632880 }, + { url = "https://files.pythonhosted.org/packages/e4/c6/6c0891fd775b4fc5613593181526ba282771682dfe7bd0206d283403bcbb/greenlet-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639a94d001fe874675b553f28a9d44faed90f9864dc57ba0afef3f8d76a18b04", size = 631638 }, + { url = "https://files.pythonhosted.org/packages/c0/50/3d8cadd4dfab17ef72bf0476cc2dacab368273ed29a79bbe66c36c6007a4/greenlet-3.2.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fe303381e7e909e42fb23e191fc69659910909fdcd056b92f6473f80ef18543", size = 580577 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/bb0fc421318c69a840e5b98fdeea29d8dcb38f43ffe8b49664aeb10cc3dc/greenlet-3.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:72c9b668454e816b5ece25daac1a42c94d1c116d5401399a11b77ce8d883110c", size = 1109788 }, + { url = "https://files.pythonhosted.org/packages/89/e9/db23a39effaef855deac9083a9054cbe34e1623dcbabed01e34a9d4174c7/greenlet-3.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6079ae990bbf944cf66bea64a09dcb56085815630955109ffa98984810d71565", size = 1133412 }, + { url = "https://files.pythonhosted.org/packages/6a/86/c33905264b43fe4806720f60124254a149857b42c1bf01bd6e136883c99f/greenlet-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e63cd2035f49376a23611fbb1643f78f8246e9d4dfd607534ec81b175ce582c2", size = 294958 }, ] [[package]] name = "grpcio" -version = "1.70.0" +version = "1.71.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/95/aa11fc09a85d91fbc7dd405dcb2a1e0256989d67bf89fa65ae24b3ba105a/grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c", size = 12549828 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/e9/f72408bac1f7b05b25e4df569b02d6b200c8e7857193aa9f1df7a3744add/grpcio-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:95469d1977429f45fe7df441f586521361e235982a0b39e33841549143ae2851", size = 5229736 }, - { url = "https://files.pythonhosted.org/packages/b3/17/e65139ea76dac7bcd8a3f17cbd37e3d1a070c44db3098d0be5e14c5bd6a1/grpcio-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ed9718f17fbdb472e33b869c77a16d0b55e166b100ec57b016dc7de9c8d236bf", size = 11432751 }, - { url = "https://files.pythonhosted.org/packages/a0/12/42de6082b4ab14a59d30b2fc7786882fdaa75813a4a4f3d4a8c4acd6ed59/grpcio-1.70.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:374d014f29f9dfdb40510b041792e0e2828a1389281eb590df066e1cc2b404e5", size = 5711439 }, - { url = "https://files.pythonhosted.org/packages/34/f8/b5a19524d273cbd119274a387bb72d6fbb74578e13927a473bc34369f079/grpcio-1.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2af68a6f5c8f78d56c145161544ad0febbd7479524a59c16b3e25053f39c87f", size = 6330777 }, - { url = "https://files.pythonhosted.org/packages/1a/67/3d6c0ad786238aac7fa93b79246fc452978fbfe9e5f86f70da8e8a2797d0/grpcio-1.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7df14b2dcd1102a2ec32f621cc9fab6695effef516efbc6b063ad749867295", size = 5944639 }, - { url = "https://files.pythonhosted.org/packages/76/0d/d9f7cbc41c2743cf18236a29b6a582f41bd65572a7144d92b80bc1e68479/grpcio-1.70.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c78b339869f4dbf89881e0b6fbf376313e4f845a42840a7bdf42ee6caed4b11f", size = 6643543 }, - { url = "https://files.pythonhosted.org/packages/fc/24/bdd7e606b3400c14330e33a4698fa3a49e38a28c9e0a831441adbd3380d2/grpcio-1.70.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58ad9ba575b39edef71f4798fdb5c7b6d02ad36d47949cd381d4392a5c9cbcd3", size = 6199897 }, - { url = "https://files.pythonhosted.org/packages/d1/33/8132eb370087960c82d01b89faeb28f3e58f5619ffe19889f57c58a19c18/grpcio-1.70.0-cp310-cp310-win32.whl", hash = "sha256:2b0d02e4b25a5c1f9b6c7745d4fa06efc9fd6a611af0fb38d3ba956786b95199", size = 3617513 }, - { url = "https://files.pythonhosted.org/packages/99/bc/0fce5cfc0ca969df66f5dca6cf8d2258abb88146bf9ab89d8cf48e970137/grpcio-1.70.0-cp310-cp310-win_amd64.whl", hash = "sha256:0de706c0a5bb9d841e353f6343a9defc9fc35ec61d6eb6111802f3aa9fef29e1", size = 4303342 }, + { url = "https://files.pythonhosted.org/packages/7c/c5/ef610b3f988cc0cc67b765f72b8e2db06a1db14e65acb5ae7810a6b7042e/grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd", size = 5210643 }, + { url = "https://files.pythonhosted.org/packages/bf/de/c84293c961622df302c0d5d07ec6e2d4cd3874ea42f602be2df09c4ad44f/grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d", size = 11308962 }, + { url = "https://files.pythonhosted.org/packages/7c/38/04c9e0dc8c904570c80faa1f1349b190b63e45d6b2782ec8567b050efa9d/grpcio-1.71.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea", size = 5699236 }, + { url = "https://files.pythonhosted.org/packages/95/96/e7be331d1298fa605ea7c9ceafc931490edd3d5b33c4f695f1a0667f3491/grpcio-1.71.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69", size = 6339767 }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7e7b7bb6bb18baf156fd4f2f5b254150dcdd6cbf0def1ee427a2fb2bfc4d/grpcio-1.71.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73", size = 5943028 }, + { url = "https://files.pythonhosted.org/packages/13/aa/5fb756175995aeb47238d706530772d9a7ac8e73bcca1b47dc145d02c95f/grpcio-1.71.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804", size = 6031841 }, + { url = "https://files.pythonhosted.org/packages/54/93/172783e01eed61f7f180617b7fa4470f504e383e32af2587f664576a7101/grpcio-1.71.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6", size = 6651039 }, + { url = "https://files.pythonhosted.org/packages/6f/99/62654b220a27ed46d3313252214f4bc66261143dc9b58004085cd0646753/grpcio-1.71.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5", size = 6198465 }, + { url = "https://files.pythonhosted.org/packages/68/35/96116de833b330abe4412cc94edc68f99ed2fa3e39d8713ff307b3799e81/grpcio-1.71.0-cp310-cp310-win32.whl", hash = "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509", size = 3620382 }, + { url = "https://files.pythonhosted.org/packages/b7/09/f32ef637e386f3f2c02effac49699229fa560ce9007682d24e9e212d2eb4/grpcio-1.71.0-cp310-cp310-win_amd64.whl", hash = "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a", size = 4280302 }, ] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] [[package]] name = "h5py" -version = "3.12.1" +version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/0c/5c2b0a88158682aeafb10c1c2b735df5bc31f165bfe192f2ee9f2a23b5f1/h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf", size = 411457 } +sdist = { url = "https://files.pythonhosted.org/packages/03/2e/a22d6a8bfa6f8be33e7febd985680fba531562795f0a9077ed1eb047bfb0/h5py-3.13.0.tar.gz", hash = "sha256:1870e46518720023da85d0895a1960ff2ce398c5671eac3b1a41ec696b7105c3", size = 414876 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/7d/b21045fbb004ad8bb6fb3be4e6ca903841722706f7130b9bba31ef2f88e3/h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda", size = 3402133 }, - { url = "https://files.pythonhosted.org/packages/29/a7/3c2a33fba1da64a0846744726fd067a92fb8abb887875a0dd8e3bac8b45d/h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3", size = 2866436 }, - { url = "https://files.pythonhosted.org/packages/1e/d0/4bf67c3937a2437c20844165766ddd1a1817ae6b9544c3743050d8e0f403/h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2", size = 5168596 }, - { url = "https://files.pythonhosted.org/packages/85/bc/e76f4b2096e0859225f5441d1b7f5e2041fffa19fc2c16756c67078417aa/h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307", size = 5341537 }, - { url = "https://files.pythonhosted.org/packages/99/bd/fb8ed45308bb97e04c02bd7aed324ba11e6a4bf9ed73967ca2a168e9cf92/h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e", size = 2990575 }, + { url = "https://files.pythonhosted.org/packages/02/8a/bc76588ff1a254e939ce48f30655a8f79fac614ca8bd1eda1a79fa276671/h5py-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5540daee2b236d9569c950b417f13fd112d51d78b4c43012de05774908dff3f5", size = 3413286 }, + { url = "https://files.pythonhosted.org/packages/19/bd/9f249ecc6c517b2796330b0aab7d2351a108fdbd00d4bb847c0877b5533e/h5py-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10894c55d46df502d82a7a4ed38f9c3fdbcb93efb42e25d275193e093071fade", size = 2915673 }, + { url = "https://files.pythonhosted.org/packages/72/71/0dd079208d7d3c3988cebc0776c2de58b4d51d8eeb6eab871330133dfee6/h5py-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb267ce4b83f9c42560e9ff4d30f60f7ae492eacf9c7ede849edf8c1b860e16b", size = 4283822 }, + { url = "https://files.pythonhosted.org/packages/d8/fa/0b6a59a1043c53d5d287effa02303bd248905ee82b25143c7caad8b340ad/h5py-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2cf6a231a07c14acd504a945a6e9ec115e0007f675bde5e0de30a4dc8d86a31", size = 4548100 }, + { url = "https://files.pythonhosted.org/packages/12/42/ad555a7ff7836c943fe97009405566dc77bcd2a17816227c10bd067a3ee1/h5py-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:851ae3a8563d87a5a0dc49c2e2529c75b8842582ccaefbf84297d2cfceeacd61", size = 2950547 }, ] [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] @@ -1137,7 +1149,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.27.1" +version = "0.30.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1148,18 +1160,18 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/d2/d6976de7542792fc077b498d64af64882b6d8bb40679284ec0bff77d5929/huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b", size = 379407 } +sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/3f/50f6b25fafdcfb1c089187a328c95081abf882309afd86f4053951507cd1/huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec", size = 450658 }, + { url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433 }, ] [[package]] name = "identify" -version = "2.6.6" +version = "2.6.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/bf/c68c46601bacd4c6fb4dd751a42b6e7087240eaabc6487f2ef7a48e0e8fc/identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251", size = 99217 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201 } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/a1/68a395c17eeefb04917034bd0a1bfa765e7654fa150cca473d669aa3afb5/identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881", size = 99083 }, + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101 }, ] [[package]] @@ -1218,7 +1230,7 @@ wheels = [ [[package]] name = "ipython" -version = "8.31.0" +version = "8.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1233,14 +1245,14 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/35/6f90fdddff7a08b7b715fccbd2427b5212c9525cd043d26fdc45bee0708d/ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b", size = 5501011 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/9f/d9a73710df947b7804bd9d93509463fb3a89e0ddc99c9fcc67279cddbeb6/ipython-8.36.0.tar.gz", hash = "sha256:24658e9fe5c5c819455043235ba59cfffded4a35936eefceceab6b192f7092ff", size = 5604997 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/60/d0feb6b6d9fe4ab89fe8fe5b47cbf6cd936bfd9f1e7ffa9d0015425aeed6/ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6", size = 821583 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/c1c9f371790b3a181e343c4815a361e5a0cc7d90ef6642d64ba5d05de289/ipython-8.36.0-py3-none-any.whl", hash = "sha256:12b913914d010dcffa2711505ec8be4bf0180742d97f1e5175e51f22086428c1", size = 831074 }, ] [[package]] name = "ipywidgets" -version = "8.1.5" +version = "8.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, @@ -1249,9 +1261,9 @@ dependencies = [ { name = "traitlets" }, { name = "widgetsnbextension" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/4c/dab2a281b07596a5fc220d49827fe6c794c66f1493d7a74f1df0640f2cc5/ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17", size = 116723 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/98/4074d9cb7e89f7ee387b41e9a4b74c8e0d6196e90b910af1cc674e1cdd3d/ipywidgets-8.1.6.tar.gz", hash = "sha256:d8ace49c66f14419fc66071371b99d01bed230bbc15d8a60233b18bfbd782851", size = 116764 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/2d/9c0b76f2f9cc0ebede1b9371b6f317243028ed60b90705863d493bae622e/ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245", size = 139767 }, + { url = "https://files.pythonhosted.org/packages/53/b8/62952729573d983d9433faacf62a52ee2e8cf46504418061ad1739967abe/ipywidgets-8.1.6-py3-none-any.whl", hash = "sha256:446e7630a1d025bdc7635e1169fcc06f2ce33b5bd41c2003edeb4a47c8d4bbb1", size = 139808 }, ] [[package]] @@ -1280,14 +1292,14 @@ wheels = [ [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] @@ -1323,11 +1335,11 @@ wheels = [ [[package]] name = "json5" -version = "0.10.0" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202 } +sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049 }, + { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079 }, ] [[package]] @@ -1380,14 +1392,14 @@ format-nongpl = [ [[package]] name = "jsonschema-specifications" -version = "2024.10.1" +version = "2025.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, ] [[package]] @@ -1458,10 +1470,11 @@ wheels = [ [[package]] name = "jupyter-events" -version = "0.11.0" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, { name = "python-json-logger" }, { name = "pyyaml" }, { name = "referencing" }, @@ -1469,9 +1482,9 @@ dependencies = [ { name = "rfc3986-validator" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/65/5791c8a979b5646ca29ea50e42b6708908b789f7ff389d1a03c1b93a1c54/jupyter_events-0.11.0.tar.gz", hash = "sha256:c0bc56a37aac29c1fbc3bcfbddb8c8c49533f9cf11f1c4e6adadba936574ab90", size = 62039 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/8c/9b65cb2cd4ea32d885993d5542244641590530836802a2e8c7449a4c61c9/jupyter_events-0.11.0-py3-none-any.whl", hash = "sha256:36399b41ce1ca45fe8b8271067d6a140ffa54cec4028e95491c93b78a855cacf", size = 19423 }, + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430 }, ] [[package]] @@ -1531,7 +1544,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.3.4" +version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1549,9 +1562,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/45/1052f842e066902b1d78126df7e2269b1b9408991e1344e167b2e429f9e1/jupyterlab-4.3.4.tar.gz", hash = "sha256:f0bb9b09a04766e3423cccc2fc23169aa2ffedcdf8713e9e0fb33cac0b6859d0", size = 21797583 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/55/3ef7e6bfe988d7df3d453cc27912846d50590c90790848594f7228c89569/jupyterlab-4.4.1.tar.gz", hash = "sha256:c75c4f33056fbd84f0b31eb44622a00c7a5f981b85adfeb198a83721f0465808", size = 23028447 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/48/af57263e53cfc220e522de047aa0993f53bab734fe812af1e03e33ac6d7c/jupyterlab-4.3.4-py3-none-any.whl", hash = "sha256:b754c2601c5be6adf87cb5a1d8495d653ffb945f021939f77776acaa94dae952", size = 11665373 }, + { url = "https://files.pythonhosted.org/packages/29/82/c8784597c5a03426c1ef20c48aff37e8cfe050ab5ca87f0d51069f02b362/jupyterlab-4.4.1-py3-none-any.whl", hash = "sha256:989bca3f9cf2d04b2022e7e657e2df6d4aca808b364810d31c4865edd968a5f7", size = 12292928 }, ] [[package]] @@ -1583,11 +1596,11 @@ wheels = [ [[package]] name = "jupyterlab-widgets" -version = "3.0.13" +version = "3.0.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/73/fa26bbb747a9ea4fca6b01453aa22990d52ab62dd61384f1ac0dc9d4e7ba/jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed", size = 203556 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/94/766b8199e8a902a4c5ee12a9407a348bbabe9fa22400758576b153d17d8e/jupyterlab_widgets-3.0.14.tar.gz", hash = "sha256:bad03e59546869f026e537e0d170e454259e6dc7048e14041707ca31e523c8a1", size = 203815 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/93/858e87edc634d628e5d752ba944c2833133a28fa87bb093e6832ced36a3e/jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54", size = 214392 }, + { url = "https://files.pythonhosted.org/packages/64/7a/f2479ba401e02f7fcbd3fc6af201eac888eaa188574b8e9df19452ab4972/jupyterlab_widgets-3.0.14-py3-none-any.whl", hash = "sha256:54c33e3306b7fca139d165d6190dc6c0627aafa5d14adfc974a4e9a3d26cb703", size = 213999 }, ] [[package]] @@ -1680,54 +1693,54 @@ wheels = [ [[package]] name = "lxml" -version = "5.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ce/2789e39eddf2b13fac29878bfa465f0910eb6b0096e29090e5176bc8cf43/lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", size = 8124570 }, - { url = "https://files.pythonhosted.org/packages/24/a8/f4010166a25d41715527129af2675981a50d3bbf7df09c5d9ab8ca24fbf9/lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", size = 4413042 }, - { url = "https://files.pythonhosted.org/packages/41/a4/7e45756cecdd7577ddf67a68b69c1db0f5ddbf0c9f65021ee769165ffc5a/lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", size = 5139213 }, - { url = "https://files.pythonhosted.org/packages/02/e2/ecf845b12323c92748077e1818b64e8b4dba509a4cb12920b3762ebe7552/lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8", size = 4838814 }, - { url = "https://files.pythonhosted.org/packages/12/91/619f9fb72cf75e9ceb8700706f7276f23995f6ad757e6d400fbe35ca4990/lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", size = 5425084 }, - { url = "https://files.pythonhosted.org/packages/25/3b/162a85a8f0fd2a3032ec3f936636911c6e9523a8e263fffcfd581ce98b54/lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", size = 4875993 }, - { url = "https://files.pythonhosted.org/packages/43/af/dd3f58cc7d946da6ae42909629a2b1d5dd2d1b583334d4af9396697d6863/lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", size = 5012462 }, - { url = "https://files.pythonhosted.org/packages/69/c1/5ea46b2d4c98f5bf5c83fffab8a0ad293c9bc74df9ecfbafef10f77f7201/lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", size = 4815288 }, - { url = "https://files.pythonhosted.org/packages/1d/51/a0acca077ad35da458f4d3f729ef98effd2b90f003440d35fc36323f8ae6/lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", size = 5472435 }, - { url = "https://files.pythonhosted.org/packages/4d/6b/0989c9368986961a6b0f55b46c80404c4b758417acdb6d87bfc3bd5f4967/lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", size = 4976354 }, - { url = "https://files.pythonhosted.org/packages/05/9e/87492d03ff604fbf656ed2bf3e2e8d28f5d58ea1f00ff27ac27b06509079/lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", size = 5029973 }, - { url = "https://files.pythonhosted.org/packages/f9/cc/9ae1baf5472af88e19e2c454b3710c1be9ecafb20eb474eeabcd88a055d2/lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", size = 4888837 }, - { url = "https://files.pythonhosted.org/packages/d2/10/5594ffaec8c120d75b17e3ad23439b740a51549a9b5fd7484b2179adfe8f/lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", size = 5530555 }, - { url = "https://files.pythonhosted.org/packages/ea/9b/de17f05377c8833343b629905571fb06cff2028f15a6f58ae2267662e341/lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", size = 5405314 }, - { url = "https://files.pythonhosted.org/packages/8a/b4/227be0f1f3cca8255925985164c3838b8b36e441ff0cc10c1d3c6bdba031/lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", size = 5079303 }, - { url = "https://files.pythonhosted.org/packages/5c/ee/19abcebb7fc40319bb71cd6adefa1ad94d09b5660228715854d6cc420713/lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", size = 3475126 }, - { url = "https://files.pythonhosted.org/packages/a1/35/183d32551447e280032b2331738cd850da435a42f850b71ebeaab42c1313/lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", size = 3805065 }, - { url = "https://files.pythonhosted.org/packages/99/f7/b73a431c8500565aa500e99e60b448d305eaf7c0b4c893c7c5a8a69cc595/lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", size = 3925431 }, - { url = "https://files.pythonhosted.org/packages/db/48/4a206623c0d093d0e3b15f415ffb4345b0bdf661a3d0b15a112948c033c7/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", size = 4216683 }, - { url = "https://files.pythonhosted.org/packages/54/47/577820c45dd954523ae8453b632d91e76da94ca6d9ee40d8c98dd86f916b/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", size = 4326732 }, - { url = "https://files.pythonhosted.org/packages/68/de/96cb6d3269bc994b4f5ede8ca7bf0840f5de0a278bc6e50cb317ff71cafa/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", size = 4218377 }, - { url = "https://files.pythonhosted.org/packages/a5/43/19b1ef6cbffa4244a217f95cc5f41a6cb4720fed33510a49670b03c5f1a0/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", size = 4351237 }, - { url = "https://files.pythonhosted.org/packages/ba/b2/6a22fb5c0885da3b00e116aee81f0b829ec9ac8f736cd414b4a09413fc7d/lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", size = 3487557 }, +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838 }, + { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827 }, + { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098 }, + { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261 }, + { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621 }, + { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231 }, + { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279 }, + { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405 }, + { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169 }, + { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691 }, + { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503 }, + { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346 }, + { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139 }, + { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609 }, + { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285 }, + { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507 }, + { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104 }, + { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319 }, + { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273 }, + { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552 }, + { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091 }, + { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862 }, ] [[package]] name = "mako" -version = "1.3.8" +version = "1.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/d9/8518279534ed7dace1795d5a47e49d5299dd0994eed1053996402a8902f9/mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8", size = 392069 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/bf/7a6a36ce2e4cafdfb202752be68850e22607fccd692847c45c1ae3c17ba6/Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627", size = 78569 }, + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509 }, ] [[package]] name = "markdown" -version = "3.7" +version = "3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210 }, ] [[package]] @@ -1762,7 +1775,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.0" +version = "3.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -1775,17 +1788,17 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/dd/fa2e1a45fce2d09f4aea3cee169760e672c8262325aa5796c49d543dc7e6/matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278", size = 36686418 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335 } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/ec/3cdff7b5239adaaacefcc4f77c316dfbbdf853c4ed2beec467e0fec31b9f/matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6", size = 8160551 }, - { url = "https://files.pythonhosted.org/packages/41/f2/b518f2c7f29895c9b167bf79f8529c63383ae94eaf49a247a4528e9a148d/matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e", size = 8034853 }, - { url = "https://files.pythonhosted.org/packages/ed/8d/45754b4affdb8f0d1a44e4e2bcd932cdf35b256b60d5eda9f455bb293ed0/matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5", size = 8446724 }, - { url = "https://files.pythonhosted.org/packages/09/5a/a113495110ae3e3395c72d82d7bc4802902e46dc797f6b041e572f195c56/matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6", size = 8583905 }, - { url = "https://files.pythonhosted.org/packages/12/b1/8b1655b4c9ed4600c817c419f7eaaf70082630efd7556a5b2e77a8a3cdaf/matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1", size = 9395223 }, - { url = "https://files.pythonhosted.org/packages/5a/85/b9a54d64585a6b8737a78a61897450403c30f39e0bd3214270bb0b96f002/matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3", size = 8025355 }, - { url = "https://files.pythonhosted.org/packages/32/5f/29def7ce4e815ab939b56280976ee35afffb3bbdb43f332caee74cb8c951/matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03", size = 8155500 }, - { url = "https://files.pythonhosted.org/packages/de/6d/d570383c9f7ca799d0a54161446f9ce7b17d6c50f2994b653514bcaa108f/matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea", size = 8032398 }, - { url = "https://files.pythonhosted.org/packages/c9/b4/680aa700d99b48e8c4393fa08e9ab8c49c0555ee6f4c9c0a5e8ea8dfde5d/matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef", size = 8587361 }, + { url = "https://files.pythonhosted.org/packages/ee/b1/f70e27cf1cd76ce2a5e1aa5579d05afe3236052c6d9b9a96325bc823a17e/matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16", size = 8163654 }, + { url = "https://files.pythonhosted.org/packages/26/af/5ec3d4636106718bb62503a03297125d4514f98fe818461bd9e6b9d116e4/matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2", size = 8037943 }, + { url = "https://files.pythonhosted.org/packages/a1/3d/07f9003a71b698b848c9925d05979ffa94a75cd25d1a587202f0bb58aa81/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698", size = 8449510 }, + { url = "https://files.pythonhosted.org/packages/12/87/9472d4513ff83b7cd864311821793ab72234fa201ab77310ec1b585d27e2/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19", size = 8586585 }, + { url = "https://files.pythonhosted.org/packages/31/9e/fe74d237d2963adae8608faeb21f778cf246dbbf4746cef87cffbc82c4b6/matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044", size = 9397911 }, + { url = "https://files.pythonhosted.org/packages/b6/1b/025d3e59e8a4281ab463162ad7d072575354a1916aba81b6a11507dfc524/matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f", size = 8052998 }, + { url = "https://files.pythonhosted.org/packages/c8/f6/10adb696d8cbeed2ab4c2e26ecf1c80dd3847bbf3891f4a0c362e0e08a5a/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc", size = 8158685 }, + { url = "https://files.pythonhosted.org/packages/3f/84/0603d917406072763e7f9bb37747d3d74d7ecd4b943a8c947cc3ae1cf7af/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4", size = 8035491 }, + { url = "https://files.pythonhosted.org/packages/fd/7d/6a8b31dd07ed856b3eae001c9129670ef75c4698fa1c2a6ac9f00a4a7054/matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779", size = 8590087 }, ] [[package]] @@ -1811,23 +1824,23 @@ wheels = [ [[package]] name = "mistune" -version = "3.1.0" +version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/6e/96fc7cb3288666c5de2c396eb0e338dc95f7a8e4920e43e38783a22d0084/mistune-3.1.0.tar.gz", hash = "sha256:dbcac2f78292b9dc066cd03b7a3a26b62d85f8159f2ea5fd28e55df79908d667", size = 94401 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/b3/743ffc3f59da380da504d84ccd1faf9a857a1445991ff19bf2ec754163c2/mistune-3.1.0-py3-none-any.whl", hash = "sha256:b05198cf6d671b3deba6c87ec6cf0d4eb7b72c524636eddb6dbf13823b52cee1", size = 53694 }, + { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410 }, ] [[package]] name = "more-itertools" -version = "10.5.0" +version = "10.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671 } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 }, ] [[package]] @@ -1845,29 +1858,31 @@ wheels = [ [[package]] name = "multidict" -version = "6.1.0" +version = "6.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, - { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, - { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, - { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, - { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, - { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, - { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, - { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, - { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, - { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, - { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, - { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, - { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, - { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, - { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, - { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/44/45e798d4cd1b5dfe41ddf36266c7aca6d954e3c7a8b0d599ad555ce2b4f8/multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5", size = 65822 }, + { url = "https://files.pythonhosted.org/packages/10/fb/9ea024f928503f8c758f8463759d21958bf27b1f7a1103df73e5022e6a7c/multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188", size = 38706 }, + { url = "https://files.pythonhosted.org/packages/6d/eb/7013316febca37414c0e1469fccadcb1a0e4315488f8f57ca5d29b384863/multidict-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7", size = 37979 }, + { url = "https://files.pythonhosted.org/packages/64/28/5a7bf4e7422613ea80f9ebc529d3845b20a422cfa94d4355504ac98047ee/multidict-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291", size = 220233 }, + { url = "https://files.pythonhosted.org/packages/52/05/b4c58850f71befde6a16548968b48331a155a80627750b150bb5962e4dea/multidict-6.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685", size = 217762 }, + { url = "https://files.pythonhosted.org/packages/99/a3/393e23bba1e9a00f95b3957acd8f5e3ee3446e78c550f593be25f9de0483/multidict-6.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf", size = 230699 }, + { url = "https://files.pythonhosted.org/packages/9c/a7/52c63069eb1a079f824257bb8045d93e692fa2eb34d08323d1fdbdfc398a/multidict-6.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1", size = 226801 }, + { url = "https://files.pythonhosted.org/packages/2c/e9/40d2b73e7d6574d91074d83477a990e3701affbe8b596010d4f5e6c7a6fa/multidict-6.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef", size = 219833 }, + { url = "https://files.pythonhosted.org/packages/e4/6a/0572b22fe63c632254f55a1c1cb7d29f644002b1d8731d6103a290edc754/multidict-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9", size = 212920 }, + { url = "https://files.pythonhosted.org/packages/33/fe/c63735db9dece0053868b2d808bcc2592a83ce1830bc98243852a2b34d42/multidict-6.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078", size = 225263 }, + { url = "https://files.pythonhosted.org/packages/47/c2/2db296d64d41525110c27ed38fadd5eb571c6b936233e75a5ea61b14e337/multidict-6.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7", size = 214249 }, + { url = "https://files.pythonhosted.org/packages/7e/74/8bc26e54c79f9a0f111350b1b28a9cacaaee53ecafccd53c90e59754d55a/multidict-6.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451", size = 221650 }, + { url = "https://files.pythonhosted.org/packages/af/d7/2ce87606e3799d9a08a941f4c170930a9895886ea8bd0eca75c44baeebe3/multidict-6.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666", size = 231235 }, + { url = "https://files.pythonhosted.org/packages/07/e1/d191a7ad3b90c613fc4b130d07a41c380e249767586148709b54d006ca17/multidict-6.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c", size = 226056 }, + { url = "https://files.pythonhosted.org/packages/24/05/a57490cf6a8d5854f4af2d17dfc54924f37fbb683986e133b76710a36079/multidict-6.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5", size = 220014 }, + { url = "https://files.pythonhosted.org/packages/5c/b1/be04fa9f08c684e9e27cca85b4ab94c10f017ec07c4c631af9c8c10bb275/multidict-6.4.3-cp310-cp310-win32.whl", hash = "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e", size = 35042 }, + { url = "https://files.pythonhosted.org/packages/d9/ca/8888f99892513001fa900eef11bafbf38ff3485109510487de009da85748/multidict-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887", size = 38506 }, + { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 }, ] [[package]] @@ -1886,6 +1901,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351 }, ] +[[package]] +name = "narwhals" +version = "1.37.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/e1/a602073ef14da8f065b08c178861cf782f08a3676492042566ae3ccc5671/narwhals-1.37.1.tar.gz", hash = "sha256:1eb8f17ff00e6c471d5afb704e9068f41657234eb73bde2ee66ad975a170015b", size = 272029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/e3/79d95ed89f46b134dd801b54375e362c505303bd739a6737efc14d0de835/narwhals-1.37.1-py3-none-any.whl", hash = "sha256:6f358a23b7351897d6efb45496dc0528918ce4ca6c8f9631594885cd873576a7", size = 332366 }, +] + [[package]] name = "nbclient" version = "0.10.2" @@ -1903,7 +1927,7 @@ wheels = [ [[package]] name = "nbconvert" -version = "7.16.5" +version = "7.16.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -1921,9 +1945,9 @@ dependencies = [ { name = "pygments" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/2c/d026c0367f2be2463d4c2f5b538e28add2bc67bc13730abb7f364ae4eb8b/nbconvert-7.16.5.tar.gz", hash = "sha256:c83467bb5777fdfaac5ebbb8e864f300b277f68692ecc04d6dab72f2d8442344", size = 856367 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/9e/2dcc9fe00cf55d95a8deae69384e9cea61816126e345754f6c75494d32ec/nbconvert-7.16.5-py3-none-any.whl", hash = "sha256:e12eac052d6fd03040af4166c563d76e7aeead2e9aadf5356db552a1784bd547", size = 258061 }, + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525 }, ] [[package]] @@ -1943,14 +1967,14 @@ wheels = [ [[package]] name = "nbstripout" -version = "0.8.0" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nbformat" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/34/e0a9ae627f02d40861656be351da44647406d7f0ce5cbc09e85abba52935/nbstripout-0.8.0.tar.gz", hash = "sha256:4b9b563c3704f9b59067627bec7d0d5c7437527ab6c3a72dd3cf895d46bf5a44", size = 26018 } +sdist = { url = "https://files.pythonhosted.org/packages/92/6e/05d7e0e35598bd0d423167295f978005912a2dcd137c88ebf36e34047dc7/nbstripout-0.8.1.tar.gz", hash = "sha256:eaac8b6b4e729e8dfe1e5df2c0f8ba44abc5a17a65448f0480141f80be230bb1", size = 26399 } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/f2/7aca174fb55ea04eb5918da199ffb6aa03258ee23e169d10a4d2a8cd4838/nbstripout-0.8.0-py2.py3-none-any.whl", hash = "sha256:b37f7b297fc6c02647d387d1049e4be8d0ecbf74640e502dce36ae93120ad420", size = 16237 }, + { url = "https://files.pythonhosted.org/packages/cf/91/93b459c456b0e4389b2b3ddb3b82cd401d022691334a0f06e92c2046e780/nbstripout-0.8.1-py2.py3-none-any.whl", hash = "sha256:79a8c8da488d98c54c112fa87185045f0271a97d84f1d46918d6a3ee561b30e7", size = 16329 }, ] [[package]] @@ -1973,7 +1997,7 @@ wheels = [ [[package]] name = "notebook" -version = "7.3.2" +version = "7.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, @@ -1982,9 +2006,9 @@ dependencies = [ { name = "notebook-shim" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/04/ac488379d5afef43402b3fb4be2857db1a09804fecf98b9b714c741b225b/notebook-7.3.2.tar.gz", hash = "sha256:705e83a1785f45b383bf3ee13cb76680b92d24f56fb0c7d2136fe1d850cd3ca8", size = 12781804 } +sdist = { url = "https://files.pythonhosted.org/packages/91/89/1b4636280f01ec948c007e700e24921135b9b76221148a405fd5287c3c17/notebook-7.4.1.tar.gz", hash = "sha256:96894962b230013ea0c0a466e4e642c5aace25ba8c86686175b69990ef628ff9", size = 13881349 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/9b/76e50ee18f183ea5fe1784a9eeaa50f2c71802e4740d6e959592b0993298/notebook-7.3.2-py3-none-any.whl", hash = "sha256:e5f85fc59b69d3618d73cf27544418193ff8e8058d5bf61d315ce4f473556288", size = 13163630 }, + { url = "https://files.pythonhosted.org/packages/41/c5/47248ed90d263e4c16f9f6b06f094105ce33f384cf135eab5a6452230d46/notebook-7.4.1-py3-none-any.whl", hash = "sha256:498f12cf567d95b20e780d62d52564ee4310248b3175e996b667b5808028e5d3", size = 14282763 }, ] [[package]] @@ -2062,11 +2086,11 @@ wheels = [ [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] @@ -2137,59 +2161,59 @@ wheels = [ [[package]] name = "pillow" -version = "11.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/1c/2dcea34ac3d7bc96a1fd1bd0a6e06a57c67167fec2cff8d95d88229a8817/pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", size = 3229983 }, - { url = "https://files.pythonhosted.org/packages/14/ca/6bec3df25e4c88432681de94a3531cc738bd85dea6c7aa6ab6f81ad8bd11/pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", size = 3101831 }, - { url = "https://files.pythonhosted.org/packages/d4/2c/668e18e5521e46eb9667b09e501d8e07049eb5bfe39d56be0724a43117e6/pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", size = 4314074 }, - { url = "https://files.pythonhosted.org/packages/02/80/79f99b714f0fc25f6a8499ecfd1f810df12aec170ea1e32a4f75746051ce/pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", size = 4394933 }, - { url = "https://files.pythonhosted.org/packages/81/aa/8d4ad25dc11fd10a2001d5b8a80fdc0e564ac33b293bdfe04ed387e0fd95/pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", size = 4353349 }, - { url = "https://files.pythonhosted.org/packages/84/7a/cd0c3eaf4a28cb2a74bdd19129f7726277a7f30c4f8424cd27a62987d864/pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", size = 4476532 }, - { url = "https://files.pythonhosted.org/packages/8f/8b/a907fdd3ae8f01c7670dfb1499c53c28e217c338b47a813af8d815e7ce97/pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", size = 4279789 }, - { url = "https://files.pythonhosted.org/packages/6f/9a/9f139d9e8cccd661c3efbf6898967a9a337eb2e9be2b454ba0a09533100d/pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", size = 4413131 }, - { url = "https://files.pythonhosted.org/packages/a8/68/0d8d461f42a3f37432203c8e6df94da10ac8081b6d35af1c203bf3111088/pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", size = 2291213 }, - { url = "https://files.pythonhosted.org/packages/14/81/d0dff759a74ba87715509af9f6cb21fa21d93b02b3316ed43bda83664db9/pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", size = 2625725 }, - { url = "https://files.pythonhosted.org/packages/ce/1f/8d50c096a1d58ef0584ddc37e6f602828515219e9d2428e14ce50f5ecad1/pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", size = 2375213 }, - { url = "https://files.pythonhosted.org/packages/fa/c5/389961578fb677b8b3244fcd934f720ed25a148b9a5cc81c91bdf59d8588/pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", size = 3198345 }, - { url = "https://files.pythonhosted.org/packages/c4/fa/803c0e50ffee74d4b965229e816af55276eac1d5806712de86f9371858fd/pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", size = 3072938 }, - { url = "https://files.pythonhosted.org/packages/dc/67/2a3a5f8012b5d8c63fe53958ba906c1b1d0482ebed5618057ef4d22f8076/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", size = 3400049 }, - { url = "https://files.pythonhosted.org/packages/e5/a0/514f0d317446c98c478d1872497eb92e7cde67003fed74f696441e647446/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", size = 3422431 }, - { url = "https://files.pythonhosted.org/packages/cd/00/20f40a935514037b7d3f87adfc87d2c538430ea625b63b3af8c3f5578e72/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", size = 3446208 }, - { url = "https://files.pythonhosted.org/packages/28/3c/7de681727963043e093c72e6c3348411b0185eab3263100d4490234ba2f6/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", size = 3509746 }, - { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 }, +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442 }, + { url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553 }, + { url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503 }, + { url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648 }, + { url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937 }, + { url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802 }, + { url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717 }, + { url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874 }, + { url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717 }, + { url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204 }, + { url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767 }, + { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727 }, + { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833 }, + { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472 }, + { url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976 }, + { url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133 }, + { url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555 }, + { url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713 }, ] [[package]] name = "pip" -version = "24.3.1" +version = "25.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +sdist = { url = "https://files.pythonhosted.org/packages/79/67/c06f625e2968c417052b3a4a0eef40656d5d4d44033e57b40ec474af1d28/pip-25.1.tar.gz", hash = "sha256:272bdd1289f80165e9070a4f881e8f9e1001bbb50378561d1af20e49bf5a2200", size = 1939624 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, + { url = "https://files.pythonhosted.org/packages/e0/f0/8a2806114cd36e282823fd4d8e88e3b94dc943c2569c350d0c826a49db38/pip-25.1-py3-none-any.whl", hash = "sha256:13b4aa0aaad055020a11bec8a1c2a70a2b2d080e12d89b962266029fff0a16ba", size = 1824948 }, ] [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, ] [[package]] name = "plotly" -version = "5.24.1" +version = "6.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "narwhals" }, { name = "packaging" }, - { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/4f/428f6d959818d7425a94c190a6b26fbc58035cbef40bf249be0b62a9aedd/plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae", size = 9479398 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/cc/e41b5f697ae403f0b50e47b7af2e36642a193085f553bf7cc1169362873a/plotly-6.0.1.tar.gz", hash = "sha256:dd8400229872b6e3c964b099be699f8d00c489a974f2cfccfad5e8240873366b", size = 8094643 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ae/580600f441f6fc05218bd6c9d5794f4aef072a7d9093b291f1c50a9db8bc/plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089", size = 19054220 }, + { url = "https://files.pythonhosted.org/packages/02/65/ad2bc85f7377f5cfba5d4466d5474423a3fb7f6a97fd807c06f92dd3e721/plotly-6.0.1-py3-none-any.whl", hash = "sha256:4714db20fea57a435692c548a4eb4fae454f7daddf15f8d8ba7e1045681d7768", size = 14805757 }, ] [[package]] @@ -2200,7 +2224,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/01/cd/f4f2b79d20a10563d [[package]] name = "pre-commit" -version = "4.0.1" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -2209,9 +2233,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, ] [[package]] @@ -2225,39 +2249,39 @@ wheels = [ [[package]] name = "prompt-toolkit" -version = "3.0.50" +version = "3.0.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, ] [[package]] name = "propcache" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/a5/0ea64c9426959ef145a938e38c832fc551843481d356713ececa9a8a64e8/propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", size = 79296 }, - { url = "https://files.pythonhosted.org/packages/76/5a/916db1aba735f55e5eca4733eea4d1973845cf77dfe67c2381a2ca3ce52d/propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", size = 45622 }, - { url = "https://files.pythonhosted.org/packages/2d/62/685d3cf268b8401ec12b250b925b21d152b9d193b7bffa5fdc4815c392c2/propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", size = 45133 }, - { url = "https://files.pythonhosted.org/packages/4d/3d/31c9c29ee7192defc05aa4d01624fd85a41cf98e5922aaed206017329944/propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212", size = 204809 }, - { url = "https://files.pythonhosted.org/packages/10/a1/e4050776f4797fc86140ac9a480d5dc069fbfa9d499fe5c5d2fa1ae71f07/propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", size = 219109 }, - { url = "https://files.pythonhosted.org/packages/c9/c0/e7ae0df76343d5e107d81e59acc085cea5fd36a48aa53ef09add7503e888/propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", size = 217368 }, - { url = "https://files.pythonhosted.org/packages/fc/e1/e0a2ed6394b5772508868a977d3238f4afb2eebaf9976f0b44a8d347ad63/propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", size = 205124 }, - { url = "https://files.pythonhosted.org/packages/50/c1/e388c232d15ca10f233c778bbdc1034ba53ede14c207a72008de45b2db2e/propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", size = 195463 }, - { url = "https://files.pythonhosted.org/packages/0a/fd/71b349b9def426cc73813dbd0f33e266de77305e337c8c12bfb0a2a82bfb/propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", size = 198358 }, - { url = "https://files.pythonhosted.org/packages/02/f2/d7c497cd148ebfc5b0ae32808e6c1af5922215fe38c7a06e4e722fe937c8/propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", size = 195560 }, - { url = "https://files.pythonhosted.org/packages/bb/57/f37041bbe5e0dfed80a3f6be2612a3a75b9cfe2652abf2c99bef3455bbad/propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", size = 196895 }, - { url = "https://files.pythonhosted.org/packages/83/36/ae3cc3e4f310bff2f064e3d2ed5558935cc7778d6f827dce74dcfa125304/propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", size = 207124 }, - { url = "https://files.pythonhosted.org/packages/8c/c4/811b9f311f10ce9d31a32ff14ce58500458443627e4df4ae9c264defba7f/propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", size = 210442 }, - { url = "https://files.pythonhosted.org/packages/18/dd/a1670d483a61ecac0d7fc4305d91caaac7a8fc1b200ea3965a01cf03bced/propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", size = 203219 }, - { url = "https://files.pythonhosted.org/packages/f9/2d/30ced5afde41b099b2dc0c6573b66b45d16d73090e85655f1a30c5a24e07/propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", size = 40313 }, - { url = "https://files.pythonhosted.org/packages/23/84/bd9b207ac80da237af77aa6e153b08ffa83264b1c7882495984fcbfcf85c/propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", size = 44428 }, - { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/56/e27c136101addf877c8291dbda1b3b86ae848f3837ce758510a0d806c92f/propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98", size = 80224 }, + { url = "https://files.pythonhosted.org/packages/63/bd/88e98836544c4f04db97eefd23b037c2002fa173dd2772301c61cd3085f9/propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180", size = 46491 }, + { url = "https://files.pythonhosted.org/packages/15/43/0b8eb2a55753c4a574fc0899885da504b521068d3b08ca56774cad0bea2b/propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71", size = 45927 }, + { url = "https://files.pythonhosted.org/packages/ad/6c/d01f9dfbbdc613305e0a831016844987a1fb4861dd221cd4c69b1216b43f/propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649", size = 206135 }, + { url = "https://files.pythonhosted.org/packages/9a/8a/e6e1c77394088f4cfdace4a91a7328e398ebed745d59c2f6764135c5342d/propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f", size = 220517 }, + { url = "https://files.pythonhosted.org/packages/19/3b/6c44fa59d6418f4239d5db8b1ece757351e85d6f3ca126dfe37d427020c8/propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229", size = 218952 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/4aeb95a1cd085e0558ab0de95abfc5187329616193a1012a6c4c930e9f7a/propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46", size = 206593 }, + { url = "https://files.pythonhosted.org/packages/da/6a/29fa75de1cbbb302f1e1d684009b969976ca603ee162282ae702287b6621/propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7", size = 196745 }, + { url = "https://files.pythonhosted.org/packages/19/7e/2237dad1dbffdd2162de470599fa1a1d55df493b16b71e5d25a0ac1c1543/propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0", size = 203369 }, + { url = "https://files.pythonhosted.org/packages/a4/bc/a82c5878eb3afb5c88da86e2cf06e1fe78b7875b26198dbb70fe50a010dc/propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519", size = 198723 }, + { url = "https://files.pythonhosted.org/packages/17/76/9632254479c55516f51644ddbf747a45f813031af5adcb8db91c0b824375/propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd", size = 200751 }, + { url = "https://files.pythonhosted.org/packages/3e/c3/a90b773cf639bd01d12a9e20c95be0ae978a5a8abe6d2d343900ae76cd71/propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259", size = 210730 }, + { url = "https://files.pythonhosted.org/packages/ed/ec/ad5a952cdb9d65c351f88db7c46957edd3d65ffeee72a2f18bd6341433e0/propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e", size = 213499 }, + { url = "https://files.pythonhosted.org/packages/83/c0/ea5133dda43e298cd2010ec05c2821b391e10980e64ee72c0a76cdbb813a/propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136", size = 207132 }, + { url = "https://files.pythonhosted.org/packages/79/dd/71aae9dec59333064cfdd7eb31a63fa09f64181b979802a67a90b2abfcba/propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42", size = 40952 }, + { url = "https://files.pythonhosted.org/packages/31/0a/49ff7e5056c17dfba62cbdcbb90a29daffd199c52f8e65e5cb09d5f53a57/propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833", size = 45163 }, + { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, ] [[package]] @@ -2308,17 +2332,19 @@ wheels = [ [[package]] name = "pyarrow" -version = "19.0.0" +version = "20.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/01/fe1fd04744c2aa038e5a11c7a4adb3d62bce09798695e54f7274b5977134/pyarrow-19.0.0.tar.gz", hash = "sha256:8d47c691765cf497aaeed4954d226568563f1b3b74ff61139f2d77876717084b", size = 1129096 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/02/1ad80ffd3c558916858a49c83b6e494a9d93009bbebc603cf0cb8263bea7/pyarrow-19.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c318eda14f6627966997a7d8c374a87d084a94e4e38e9abbe97395c215830e0c", size = 30686262 }, - { url = "https://files.pythonhosted.org/packages/1b/f0/adab5f142eb8203db8bfbd3a816816e37a85423ae684567e7f3555658315/pyarrow-19.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:62ef8360ff256e960f57ce0299090fb86423afed5e46f18f1225f960e05aae3d", size = 32100005 }, - { url = "https://files.pythonhosted.org/packages/94/8b/e674083610e5efc48d2f205c568d842cdfdf683d12f9ff0d546e38757722/pyarrow-19.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2795064647add0f16563e57e3d294dbfc067b723f0fd82ecd80af56dad15f503", size = 41144815 }, - { url = "https://files.pythonhosted.org/packages/d5/fb/2726241a792b7f8a58789e5a63d1be9a5a4059206318fd0ff9485a578952/pyarrow-19.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a218670b26fb1bc74796458d97bcab072765f9b524f95b2fccad70158feb8b17", size = 42180380 }, - { url = "https://files.pythonhosted.org/packages/7d/09/7aef12446d8e7002dfc07bb7bc71f594c1d5844ca78b364a49f07efb65b1/pyarrow-19.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:66732e39eaa2247996a6b04c8aa33e3503d351831424cdf8d2e9a0582ac54b34", size = 40515021 }, - { url = "https://files.pythonhosted.org/packages/31/55/f05fc5608cc96060c2b24de505324d641888bd62d4eed2fa1dacd872a1e1/pyarrow-19.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e675a3ad4732b92d72e4d24009707e923cab76b0d088e5054914f11a797ebe44", size = 42067488 }, - { url = "https://files.pythonhosted.org/packages/f0/01/097653cec7a944c16313cb748a326771133c142034b252076bd84743b98d/pyarrow-19.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f094742275586cdd6b1a03655ccff3b24b2610c3af76f810356c4c71d24a2a6c", size = 25276726 }, + { url = "https://files.pythonhosted.org/packages/5b/23/77094eb8ee0dbe88441689cb6afc40ac312a1e15d3a7acc0586999518222/pyarrow-20.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c7dd06fd7d7b410ca5dc839cc9d485d2bc4ae5240851bcd45d85105cc90a47d7", size = 30832591 }, + { url = "https://files.pythonhosted.org/packages/c3/d5/48cc573aff00d62913701d9fac478518f693b30c25f2c157550b0b2565cb/pyarrow-20.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d5382de8dc34c943249b01c19110783d0d64b207167c728461add1ecc2db88e4", size = 32273686 }, + { url = "https://files.pythonhosted.org/packages/37/df/4099b69a432b5cb412dd18adc2629975544d656df3d7fda6d73c5dba935d/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6415a0d0174487456ddc9beaead703d0ded5966129fa4fd3114d76b5d1c5ceae", size = 41337051 }, + { url = "https://files.pythonhosted.org/packages/4c/27/99922a9ac1c9226f346e3a1e15e63dee6f623ed757ff2893f9d6994a69d3/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15aa1b3b2587e74328a730457068dc6c89e6dcbf438d4369f572af9d320a25ee", size = 42404659 }, + { url = "https://files.pythonhosted.org/packages/21/d1/71d91b2791b829c9e98f1e0d85be66ed93aff399f80abb99678511847eaa/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5605919fbe67a7948c1f03b9f3727d82846c053cd2ce9303ace791855923fd20", size = 40695446 }, + { url = "https://files.pythonhosted.org/packages/f1/ca/ae10fba419a6e94329707487835ec721f5a95f3ac9168500bcf7aa3813c7/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a5704f29a74b81673d266e5ec1fe376f060627c2e42c5c7651288ed4b0db29e9", size = 42278528 }, + { url = "https://files.pythonhosted.org/packages/7a/a6/aba40a2bf01b5d00cf9cd16d427a5da1fad0fb69b514ce8c8292ab80e968/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:00138f79ee1b5aca81e2bdedb91e3739b987245e11fa3c826f9e57c5d102fb75", size = 42918162 }, + { url = "https://files.pythonhosted.org/packages/93/6b/98b39650cd64f32bf2ec6d627a9bd24fcb3e4e6ea1873c5e1ea8a83b1a18/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2d67ac28f57a362f1a2c1e6fa98bfe2f03230f7e15927aecd067433b1e70ce8", size = 44550319 }, + { url = "https://files.pythonhosted.org/packages/ab/32/340238be1eb5037e7b5de7e640ee22334417239bc347eadefaf8c373936d/pyarrow-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a8b029a07956b8d7bd742ffca25374dd3f634b35e46cc7a7c3fa4c75b297191", size = 25770759 }, ] [[package]] @@ -2332,14 +2358,14 @@ wheels = [ [[package]] name = "pyasn1-modules" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, ] [[package]] @@ -2353,86 +2379,89 @@ wheels = [ [[package]] name = "pycryptodome" -version = "3.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, - { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, - { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, - { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, - { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, - { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, - { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, - { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, - { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, - { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, - { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, - { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, - { url = "https://files.pythonhosted.org/packages/08/16/ae464d4ac338c1dd41f89c41f9488e54f7d2a3acf93bb920bb193b99f8e3/pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", size = 1615855 }, - { url = "https://files.pythonhosted.org/packages/1e/8c/b0cee957eee1950ce7655006b26a8894cee1dc4b8747ae913684352786eb/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", size = 1650018 }, - { url = "https://files.pythonhosted.org/packages/93/4d/d7138068089b99f6b0368622e60f97a577c936d75f533552a82613060c58/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", size = 1687977 }, - { url = "https://files.pythonhosted.org/packages/96/02/90ae1ac9f28be4df0ed88c127bf4acc1b102b40053e172759d4d1c54d937/pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6", size = 1788273 }, +version = "3.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/e6/099310419df5ada522ff34ffc2f1a48a11b37fc6a76f51a6854c182dbd3e/pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723", size = 4917300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/65/a05831c3e4bcd1bf6c2a034e399f74b3d6f30bb4e37e36b9c310c09dc8c0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6", size = 2490637 }, + { url = "https://files.pythonhosted.org/packages/5c/76/ff3c2e7a60d17c080c4c6120ebaf60f38717cd387e77f84da4dcf7f64ff0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00", size = 1635372 }, + { url = "https://files.pythonhosted.org/packages/cc/7f/cc5d6da0dbc36acd978d80a72b228e33aadaec9c4f91c93221166d8bdc05/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc", size = 2177456 }, + { url = "https://files.pythonhosted.org/packages/92/65/35f5063e68790602d892ad36e35ac723147232a9084d1999630045c34593/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d", size = 2263744 }, + { url = "https://files.pythonhosted.org/packages/cc/67/46acdd35b1081c3dbc72dc466b1b95b80d2f64cad3520f994a9b6c5c7d00/pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89", size = 2303356 }, + { url = "https://files.pythonhosted.org/packages/3d/f9/a4f8a83384626098e3f55664519bec113002b9ef751887086ae63a53135a/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5", size = 2176714 }, + { url = "https://files.pythonhosted.org/packages/88/65/e5f8c3a885f70a6e05c84844cd5542120576f4369158946e8cfc623a464d/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8", size = 2337329 }, + { url = "https://files.pythonhosted.org/packages/b8/2a/25e0be2b509c28375c7f75c7e8d8d060773f2cce4856a1654276e3202339/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772", size = 2262255 }, + { url = "https://files.pythonhosted.org/packages/41/58/60917bc4bbd91712e53ce04daf237a74a0ad731383a01288130672994328/pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb", size = 1763403 }, + { url = "https://files.pythonhosted.org/packages/55/f4/244c621afcf7867e23f63cfd7a9630f14cfe946c9be7e566af6c3915bcde/pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627", size = 1794568 }, + { url = "https://files.pythonhosted.org/packages/cd/13/16d3a83b07f949a686f6cfd7cfc60e57a769ff502151ea140ad67b118e26/pycryptodome-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:98fd9da809d5675f3a65dcd9ed384b9dc67edab6a4cda150c5870a8122ec961d", size = 1700779 }, + { url = "https://files.pythonhosted.org/packages/13/af/16d26f7dfc5fd7696ea2c91448f937b51b55312b5bed44f777563e32a4fe/pycryptodome-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:37ddcd18284e6b36b0a71ea495a4c4dca35bb09ccc9bfd5b91bfaf2321f131c1", size = 1775230 }, + { url = "https://files.pythonhosted.org/packages/37/c3/e3423e72669ca09f141aae493e1feaa8b8475859898b04f57078280a61c4/pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13", size = 1618698 }, + { url = "https://files.pythonhosted.org/packages/f9/b7/35eec0b3919cafea362dcb68bb0654d9cb3cde6da6b7a9d8480ce0bf203a/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333", size = 1666957 }, + { url = "https://files.pythonhosted.org/packages/b0/1f/f49bccdd8d61f1da4278eb0d6aee7f988f1a6ec4056b0c2dc51eda45ae27/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9", size = 1659242 }, + { url = "https://files.pythonhosted.org/packages/95/43/a01dcf1ed39c9a9e9c9d3f9d98040deeceedaa7cdf043e175251f2e13f44/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad", size = 1697246 }, + { url = "https://files.pythonhosted.org/packages/3b/49/195842931f9ee6f14cd63ef85e06b93073463ed59601fb283ba9b813cd53/pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6", size = 1797436 }, ] [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, + { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, + { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, + { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, ] [[package]] name = "pydantic-settings" -version = "2.7.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, ] [[package]] @@ -2446,29 +2475,29 @@ wheels = [ [[package]] name = "pymupdf" -version = "1.25.2" +version = "1.25.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/fc/dd8776dc5c2f8cf0e51cf81a5f1de3840996bed7ca03ec768b0733024fb9/pymupdf-1.25.2.tar.gz", hash = "sha256:9ea88ff1b3ccb359620f106a6fd5ba6877d959d21d78272052c3496ceede6eec", size = 63814915 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/af/3d5d363241b9a74470273cf1534436f13a0a61fc5ef6efd19e5afe9de812/pymupdf-1.25.5.tar.gz", hash = "sha256:5f96311cacd13254c905f6654a004a0a2025b71cabc04fda667f5472f72c15a0", size = 69812626 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/34/8c3d82719d118beb48fded78fcab7cbe9ac3bf1906dc87a9ca4fd950087d/pymupdf-1.25.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59dea22b633cc4fc13670b4c5db50d71f8cd4f420814420f33ce47ddcb61e1f6", size = 19336722 }, - { url = "https://files.pythonhosted.org/packages/4f/ec/c7f742f56ee42be27b3afdbf3364da12f03e309f6638e666a7816d9eef23/pymupdf-1.25.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e8b8a874497cd0deee89a6a4fb76a3a08173c8d39e88fc7cf715764ec5a243e9", size = 18570847 }, - { url = "https://files.pythonhosted.org/packages/9d/27/557ee235aded5185e4824459e1540142fbb9323e1b83f77cbefe2e2c4e1e/pymupdf-1.25.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f61e5cdb25b86eb28d34aa3557b49ecf9e361d5f5cd3b1660406f8f0bf813af7", size = 19430802 }, - { url = "https://files.pythonhosted.org/packages/0e/de/35fde3d49e0d187b95ab64cc61b4d275ebc7fd4f45e152b206b0e17e6b69/pymupdf-1.25.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8cfa7a97d78f813d286ecba32369059d88073edd1e5cf105f4cd0811f71925", size = 19994315 }, - { url = "https://files.pythonhosted.org/packages/9d/d3/a8a09b550c62306c76e1c2d892c0890287470164d7941aea35330cceee4d/pymupdf-1.25.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:295505fe1ecb7c7b57d4124d373e207ea311d8e40bc7ac3016d8ec2d60b091e9", size = 21117143 }, - { url = "https://files.pythonhosted.org/packages/ef/ac/fc4f37c7620a20d25443868ed665291e96f283eda068cda673e9edebf5f0/pymupdf-1.25.2-cp39-abi3-win32.whl", hash = "sha256:b9488c8b82bb9be36fb13ee0c8d43b0ddcc50af83b61da01e6040413d9e67da6", size = 15084555 }, - { url = "https://files.pythonhosted.org/packages/64/8e/1d0ff215b37343c7e0bec4d571f1413e4f76a416591276b97081f1814710/pymupdf-1.25.2-cp39-abi3-win_amd64.whl", hash = "sha256:1b4ca6f5780d319a08dff885a5a0e3585c5d7af04dcfa063c535b88371fd91c1", size = 16531823 }, + { url = "https://files.pythonhosted.org/packages/85/5f/153d6c338291448e182648844849d13938a62a82a3e4a9b0907d9b381148/pymupdf-1.25.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cde4e1c9cfb09c0e1e9c2b7f4b787dd6bb34a32cfe141a4675e24af7c0c25dd3", size = 19364722 }, + { url = "https://files.pythonhosted.org/packages/4e/55/43b64fa6cd048d2ea4574c045b5ac05d023254b91c2c703185f6f8a77b30/pymupdf-1.25.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5a35e2725fae0ab57f058dff77615c15eb5961eac50ba04f41ebc792cd8facad", size = 18606161 }, + { url = "https://files.pythonhosted.org/packages/8b/22/29edb3236aed2f99a7922699fd71183e2f6cdde3c3884670158ae4dcf3ea/pymupdf-1.25.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d94b800e9501929c42283d39bc241001dd87fdeea297b5cb40d5b5714534452f", size = 19467121 }, + { url = "https://files.pythonhosted.org/packages/18/12/95e2ebe2933f94800fdeafd87bc281a790e1dc947b147c3d101df4f73703/pymupdf-1.25.5-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee22155d3a634642d76553204867d862ae1bdd9f7cf70c0797d8127ebee6bed5", size = 20030310 }, + { url = "https://files.pythonhosted.org/packages/bd/db/b4edec9e731ea7c2b74bf28b9091ed4e919d5c7f889ef86352b7fd416197/pymupdf-1.25.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6ed7fc25271004d6d3279c20a80cb2bb4cda3efa9f9088dcc07cd790eca0bc63", size = 21293562 }, + { url = "https://files.pythonhosted.org/packages/ec/47/682a8ddce650e09f5de6809c9bce926b2493a19b7f9537d80d4646989670/pymupdf-1.25.5-cp39-abi3-win32.whl", hash = "sha256:65e18ddb37fe8ec4edcdbebe9be3a8486b6a2f42609d0a142677e42f3a0614f8", size = 15110464 }, + { url = "https://files.pythonhosted.org/packages/71/c2/a9059607f80dcaf2392f991748cfc53456820392c0220cff02572653512a/pymupdf-1.25.5-cp39-abi3-win_amd64.whl", hash = "sha256:7f44bc3d03ea45b2f68c96464f96105e8c7908896f2fb5e8c04f1fb8dae7981e", size = 16579671 }, ] [[package]] name = "pymupdf4llm" -version = "0.0.17" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pymupdf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/3c/1a530a410bdf76d83289bf30b3b86236d338b3f5f21842790c2cf7e9c1f6/pymupdf4llm-0.0.17.tar.gz", hash = "sha256:27287ef9fe0217cf37841a3ef2bcf70da2553c43d95ea39b664a6de6485678c3", size = 25180 } +sdist = { url = "https://files.pythonhosted.org/packages/e6/f4/39f041220a7e328f4c71cc72953e3ff29ed188396b2ba1b45daca235f50f/pymupdf4llm-0.0.22.tar.gz", hash = "sha256:b63a0fbbf3b1b1b42f239e2e23797f3b25d2163470dc7f4f84841b7bf3618c2c", size = 28089 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/af/1576ecfc8a62d31c0c8b34b856e52f6b05f1d76546dbac0e1d037f044a9e/pymupdf4llm-0.0.17-py3-none-any.whl", hash = "sha256:26de9996945f15e3ca507908f80dc18a959f5b5214bb2e302c7f7034089665a0", size = 26190 }, + { url = "https://files.pythonhosted.org/packages/d6/ab/0f3f2f6a6d8521cfdb8226f63a50d22e7c6ca5e2fb72f1fbf56efda04218/pymupdf4llm-0.0.22-py3-none-any.whl", hash = "sha256:80716a74e0895d8a56a2edaad02484308ea93bd8aa80b8ef3d3d90e4e49ce8b6", size = 28707 }, ] [[package]] @@ -2482,11 +2511,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, ] [[package]] @@ -2512,20 +2541,20 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] name = "python-json-logger" -version = "3.2.1" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/c4/358cd13daa1d912ef795010897a483ab2f0b41c9ea1b35235a8b2f7d15a7/python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008", size = 16287 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/72/2f30cf26664fcfa0bd8ec5ee62ec90c03bd485e4a294d92aabc76c5203a5/python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090", size = 14924 }, + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163 }, ] [[package]] @@ -2596,30 +2625,30 @@ wheels = [ [[package]] name = "pytz" -version = "2024.2" +version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, ] [[package]] name = "pywin32" -version = "308" +version = "310" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/a6/3e9f2c474895c1bb61b11fa9640be00067b5c5b363c501ee9c3fa53aec01/pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e", size = 5927028 }, - { url = "https://files.pythonhosted.org/packages/d9/b4/84e2463422f869b4b718f79eb7530a4c1693e96b8a4e5e968de38be4d2ba/pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e", size = 6558484 }, - { url = "https://files.pythonhosted.org/packages/9f/8f/fb84ab789713f7c6feacaa08dad3ec8105b88ade8d1c4f0f0dfcaaa017d6/pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c", size = 7971454 }, + { url = "https://files.pythonhosted.org/packages/95/da/a5f38fffbba2fb99aa4aa905480ac4b8e83ca486659ac8c95bce47fb5276/pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1", size = 8848240 }, + { url = "https://files.pythonhosted.org/packages/aa/fe/d873a773324fa565619ba555a82c9dabd677301720f3660a731a5d07e49a/pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d", size = 9601854 }, + { url = "https://files.pythonhosted.org/packages/3c/84/1a8e3d7a15490d28a5d816efa229ecb4999cdc51a7c30dd8914f669093b8/pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213", size = 8522963 }, ] [[package]] name = "pywinpty" -version = "2.0.14" +version = "2.0.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/82/90f8750423cba4b9b6c842df227609fb60704482d7abf6dd47e2babc055a/pywinpty-2.0.14.tar.gz", hash = "sha256:18bd9529e4a5daf2d9719aa17788ba6013e594ae94c5a0c27e83df3278b0660e", size = 27769 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/7c/917f9c4681bb8d34bfbe0b79d36bbcd902651aeab48790df3d30ba0202fb/pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2", size = 29017 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/09/56376af256eab8cc5f8982a3b138d387136eca27fa1a8a68660e8ed59e4b/pywinpty-2.0.14-cp310-none-win_amd64.whl", hash = "sha256:0b149c2918c7974f575ba79f5a4aad58bd859a52fa9eb1296cc22aa412aa411f", size = 1397115 }, + { url = "https://files.pythonhosted.org/packages/a6/b7/855db919ae526d2628f3f2e6c281c4cdff7a9a8af51bb84659a9f07b1861/pywinpty-2.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:8e7f5de756a615a38b96cd86fa3cd65f901ce54ce147a3179c45907fa11b4c4e", size = 1405161 }, ] [[package]] @@ -2641,59 +2670,58 @@ wheels = [ [[package]] name = "pyzmq" -version = "26.2.0" +version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/a8/9837c39aba390eb7d01924ace49d761c8dbe7bc2d6082346d00c8332e431/pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629", size = 1340058 }, - { url = "https://files.pythonhosted.org/packages/a2/1f/a006f2e8e4f7d41d464272012695da17fb95f33b54342612a6890da96ff6/pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b", size = 1008818 }, - { url = "https://files.pythonhosted.org/packages/b6/09/b51b6683fde5ca04593a57bbe81788b6b43114d8f8ee4e80afc991e14760/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764", size = 673199 }, - { url = "https://files.pythonhosted.org/packages/c9/78/486f3e2e824f3a645238332bf5a4c4b4477c3063033a27c1e4052358dee2/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c", size = 911762 }, - { url = "https://files.pythonhosted.org/packages/5e/3b/2eb1667c9b866f53e76ee8b0c301b0469745a23bd5a87b7ee3d5dd9eb6e5/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a", size = 868773 }, - { url = "https://files.pythonhosted.org/packages/16/29/ca99b4598a9dc7e468b5417eda91f372b595be1e3eec9b7cbe8e5d3584e8/pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88", size = 868834 }, - { url = "https://files.pythonhosted.org/packages/ad/e5/9efaeb1d2f4f8c50da04144f639b042bc52869d3a206d6bf672ab3522163/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f", size = 1202861 }, - { url = "https://files.pythonhosted.org/packages/c3/62/c721b5608a8ac0a69bb83cbb7d07a56f3ff00b3991a138e44198a16f94c7/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282", size = 1515304 }, - { url = "https://files.pythonhosted.org/packages/87/84/e8bd321aa99b72f48d4606fc5a0a920154125bd0a4608c67eab742dab087/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea", size = 1414712 }, - { url = "https://files.pythonhosted.org/packages/cd/cd/420e3fd1ac6977b008b72e7ad2dae6350cc84d4c5027fc390b024e61738f/pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2", size = 578113 }, - { url = "https://files.pythonhosted.org/packages/5c/57/73930d56ed45ae0cb4946f383f985c855c9b3d4063f26416998f07523c0e/pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971", size = 641631 }, - { url = "https://files.pythonhosted.org/packages/61/d2/ae6ac5c397f1ccad59031c64beaafce7a0d6182e0452cc48f1c9c87d2dd0/pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa", size = 543528 }, - { url = "https://files.pythonhosted.org/packages/53/fb/36b2b2548286e9444e52fcd198760af99fd89102b5be50f0660fcfe902df/pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072", size = 906955 }, - { url = "https://files.pythonhosted.org/packages/77/8f/6ce54f8979a01656e894946db6299e2273fcee21c8e5fa57c6295ef11f57/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1", size = 565701 }, - { url = "https://files.pythonhosted.org/packages/ee/1c/bf8cd66730a866b16db8483286078892b7f6536f8c389fb46e4beba0a970/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d", size = 794312 }, - { url = "https://files.pythonhosted.org/packages/71/43/91fa4ff25bbfdc914ab6bafa0f03241d69370ef31a761d16bb859f346582/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca", size = 752775 }, - { url = "https://files.pythonhosted.org/packages/ec/d2/3b2ab40f455a256cb6672186bea95cd97b459ce4594050132d71e76f0d6f/pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c", size = 550762 }, + { url = "https://files.pythonhosted.org/packages/38/b8/af1d814ffc3ff9730f9a970cbf216b6f078e5d251a25ef5201d7bc32a37c/pyzmq-26.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918", size = 1339238 }, + { url = "https://files.pythonhosted.org/packages/ee/e4/5aafed4886c264f2ea6064601ad39c5fc4e9b6539c6ebe598a859832eeee/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315", size = 672848 }, + { url = "https://files.pythonhosted.org/packages/79/39/026bf49c721cb42f1ef3ae0ee3d348212a7621d2adb739ba97599b6e4d50/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d52d62edc96787f5c1dfa6c6ccff9b581cfae5a70d94ec4c8da157656c73b5b", size = 911299 }, + { url = "https://files.pythonhosted.org/packages/03/23/b41f936a9403b8f92325c823c0f264c6102a0687a99c820f1aaeb99c1def/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1410c3a3705db68d11eb2424d75894d41cff2f64d948ffe245dd97a9debfebf4", size = 867920 }, + { url = "https://files.pythonhosted.org/packages/c1/3e/2de5928cdadc2105e7c8f890cc5f404136b41ce5b6eae5902167f1d5641c/pyzmq-26.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7dacb06a9c83b007cc01e8e5277f94c95c453c5851aac5e83efe93e72226353f", size = 862514 }, + { url = "https://files.pythonhosted.org/packages/ce/57/109569514dd32e05a61d4382bc88980c95bfd2f02e58fea47ec0ccd96de1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6bab961c8c9b3a4dc94d26e9b2cdf84de9918931d01d6ff38c721a83ab3c0ef5", size = 1204494 }, + { url = "https://files.pythonhosted.org/packages/aa/02/dc51068ff2ca70350d1151833643a598625feac7b632372d229ceb4de3e1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a5c09413b924d96af2aa8b57e76b9b0058284d60e2fc3730ce0f979031d162a", size = 1514525 }, + { url = "https://files.pythonhosted.org/packages/48/2a/a7d81873fff0645eb60afaec2b7c78a85a377af8f1d911aff045d8955bc7/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d489ac234d38e57f458fdbd12a996bfe990ac028feaf6f3c1e81ff766513d3b", size = 1414659 }, + { url = "https://files.pythonhosted.org/packages/ef/ea/813af9c42ae21845c1ccfe495bd29c067622a621e85d7cda6bc437de8101/pyzmq-26.4.0-cp310-cp310-win32.whl", hash = "sha256:dea1c8db78fb1b4b7dc9f8e213d0af3fc8ecd2c51a1d5a3ca1cde1bda034a980", size = 580348 }, + { url = "https://files.pythonhosted.org/packages/20/68/318666a89a565252c81d3fed7f3b4c54bd80fd55c6095988dfa2cd04a62b/pyzmq-26.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa59e1f5a224b5e04dc6c101d7186058efa68288c2d714aa12d27603ae93318b", size = 643838 }, + { url = "https://files.pythonhosted.org/packages/91/f8/fb1a15b5f4ecd3e588bfde40c17d32ed84b735195b5c7d1d7ce88301a16f/pyzmq-26.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:a651fe2f447672f4a815e22e74630b6b1ec3a1ab670c95e5e5e28dcd4e69bbb5", size = 559565 }, + { url = "https://files.pythonhosted.org/packages/47/03/96004704a84095f493be8d2b476641f5c967b269390173f85488a53c1c13/pyzmq-26.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:98d948288ce893a2edc5ec3c438fe8de2daa5bbbd6e2e865ec5f966e237084ba", size = 834408 }, + { url = "https://files.pythonhosted.org/packages/e4/7f/68d8f3034a20505db7551cb2260248be28ca66d537a1ac9a257913d778e4/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f34f5c9e0203ece706a1003f1492a56c06c0632d86cb77bcfe77b56aacf27b", size = 569580 }, + { url = "https://files.pythonhosted.org/packages/9b/a6/2b0d6801ec33f2b2a19dd8d02e0a1e8701000fec72926e6787363567d30c/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80c9b48aef586ff8b698359ce22f9508937c799cc1d2c9c2f7c95996f2300c94", size = 798250 }, + { url = "https://files.pythonhosted.org/packages/96/2a/0322b3437de977dcac8a755d6d7ce6ec5238de78e2e2d9353730b297cf12/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f2a5b74009fd50b53b26f65daff23e9853e79aa86e0aa08a53a7628d92d44a", size = 756758 }, + { url = "https://files.pythonhosted.org/packages/c2/33/43704f066369416d65549ccee366cc19153911bec0154da7c6b41fca7e78/pyzmq-26.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:61c5f93d7622d84cb3092d7f6398ffc77654c346545313a3737e266fc11a3beb", size = 555371 }, ] [[package]] name = "rapidfuzz" -version = "3.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/aa/25e5a20131369d82c7b8288c99c2c3011ec47a3f5953ccc9cb8145720be5/rapidfuzz-3.11.0.tar.gz", hash = "sha256:a53ca4d3f52f00b393fab9b5913c5bafb9afc27d030c8a1db1283da6917a860f", size = 57983000 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/70/820ebf9eb22ad97b9e0bb9fd1ad8c6be4c8db5a0974d12ce27b5c9a30db0/rapidfuzz-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb8a54543d16ab1b69e2c5ed96cabbff16db044a50eddfc028000138ca9ddf33", size = 1954240 }, - { url = "https://files.pythonhosted.org/packages/41/bc/e39abdc28160d8147ccab0aa922a29be50529dcf149615a68a324ff6f9b1/rapidfuzz-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231c8b2efbd7f8d2ecd1ae900363ba168b8870644bb8f2b5aa96e4a7573bde19", size = 1427139 }, - { url = "https://files.pythonhosted.org/packages/b6/2d/19b8e5d80257b13d73ba994552b78a69ac2ed70f1de716f1b02fcb84d09c/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54e7f442fb9cca81e9df32333fb075ef729052bcabe05b0afc0441f462299114", size = 1419602 }, - { url = "https://files.pythonhosted.org/packages/8c/82/1fc80cc531ec712872025c19118d78eb23aff09c7144b380c2c4b544b0d1/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:906f1f2a1b91c06599b3dd1be207449c5d4fc7bd1e1fa2f6aef161ea6223f165", size = 5635370 }, - { url = "https://files.pythonhosted.org/packages/3c/5c/007b90af25f98e301b5f7a095059b09f602701443d555724c9226a45514c/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed59044aea9eb6c663112170f2399b040d5d7b162828b141f2673e822093fa8", size = 1680848 }, - { url = "https://files.pythonhosted.org/packages/01/04/e481530eff5d1cf337b86a3095dd4de0b758c37291e51eb0d8c4f7d49719/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cb1965a28b0fa64abdee130c788a0bc0bb3cf9ef7e3a70bf055c086c14a3d7e", size = 1682131 }, - { url = "https://files.pythonhosted.org/packages/10/15/b0ec18edfe6146d8915679644ab7584cd0165724d6a53bcc43bd59f8edb5/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b488b244931d0291412917e6e46ee9f6a14376625e150056fe7c4426ef28225", size = 3134097 }, - { url = "https://files.pythonhosted.org/packages/8b/0e/cf0a5d62977381bca981fc171fd6c85dc52ca1239eaacf9c1d38978c5866/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f0ba13557fec9d5ffc0a22826754a7457cc77f1b25145be10b7bb1d143ce84c6", size = 2332928 }, - { url = "https://files.pythonhosted.org/packages/dc/71/568d383eb36586c9e7e13f1327203e2be0938e5ff070c1fa2a99b418808e/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3871fa7dfcef00bad3c7e8ae8d8fd58089bad6fb21f608d2bf42832267ca9663", size = 6940409 }, - { url = "https://files.pythonhosted.org/packages/ba/23/02972657d69e6d3aae2cdbd67debad080410ff9ef8849d8eab5e580a48a5/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b2669eafee38c5884a6e7cc9769d25c19428549dcdf57de8541cf9e82822e7db", size = 2715928 }, - { url = "https://files.pythonhosted.org/packages/17/17/d964d770faa4e25e125618c00e31607cf8ce639d518fc29d200edf06cfda/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ffa1bb0e26297b0f22881b219ffc82a33a3c84ce6174a9d69406239b14575bd5", size = 3265078 }, - { url = "https://files.pythonhosted.org/packages/bc/13/a117412b1e4ed0bb23b9891a45a59812d96fde8c076b8b8b828aa7ca3710/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:45b15b8a118856ac9caac6877f70f38b8a0d310475d50bc814698659eabc1cdb", size = 4169215 }, - { url = "https://files.pythonhosted.org/packages/9f/0d/89ef496aedf885db4bfe7f46ac6727666afe0d9d8ca5b4f9c7cc8eef0378/rapidfuzz-3.11.0-cp310-cp310-win32.whl", hash = "sha256:22033677982b9c4c49676f215b794b0404073f8974f98739cb7234e4a9ade9ad", size = 1841736 }, - { url = "https://files.pythonhosted.org/packages/47/9a/69019f4e9c8a42e4aca0169dbae71602aba4e0fa4c5e84515f3ed682e59a/rapidfuzz-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:be15496e7244361ff0efcd86e52559bacda9cd975eccf19426a0025f9547c792", size = 1614955 }, - { url = "https://files.pythonhosted.org/packages/37/65/6fb036e39d175299ce44e5186ee2d08b9ea02d732ed6dbd70280f63b4eba/rapidfuzz-3.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:714a7ba31ba46b64d30fccfe95f8013ea41a2e6237ba11a805a27cdd3bce2573", size = 863543 }, - { url = "https://files.pythonhosted.org/packages/30/5a/8ac67667663d24cc4d4b76f63783e58ef03e4d4843d02dab6b2f8470ea5e/rapidfuzz-3.11.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f06e3c4c0a8badfc4910b9fd15beb1ad8f3b8fafa8ea82c023e5e607b66a78e4", size = 1853100 }, - { url = "https://files.pythonhosted.org/packages/dc/72/b043c26e93fb1bc5dfab1e5dd0f8d2f6135c2aa48e6db0660d4ecc5b157a/rapidfuzz-3.11.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fe7aaf5a54821d340d21412f7f6e6272a9b17a0cbafc1d68f77f2fc11009dcd5", size = 1361961 }, - { url = "https://files.pythonhosted.org/packages/5c/4a/29916c0dd853d22ef7b988af43f4e34d327581e16f60b4c9b0f229fa306c/rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25398d9ac7294e99876a3027ffc52c6bebeb2d702b1895af6ae9c541ee676702", size = 1354313 }, - { url = "https://files.pythonhosted.org/packages/41/39/f352af4ede7faeeea20bae2537f1fa60c3bbbf2696f0f2f3dda696745239/rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a52eea839e4bdc72c5e60a444d26004da00bb5bc6301e99b3dde18212e41465", size = 5478019 }, - { url = "https://files.pythonhosted.org/packages/99/8e/86f8a11ac0edda63ff5314d992aa1576fff5d8233f4310d46a6bb0551122/rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c87319b0ab9d269ab84f6453601fd49b35d9e4a601bbaef43743f26fabf496c", size = 3056881 }, - { url = "https://files.pythonhosted.org/packages/98/53/222dceb24a83c7d7d76086b6d5bfd3d6aa9988ea73d356d287b5c437c0d5/rapidfuzz-3.11.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3048c6ed29d693fba7d2a7caf165f5e0bb2b9743a0989012a98a47b975355cca", size = 1543944 }, +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/27/ca10b3166024ae19a7e7c21f73c58dfd4b7fef7420e5497ee64ce6b73453/rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255", size = 1998899 }, + { url = "https://files.pythonhosted.org/packages/f0/38/c4c404b13af0315483a6909b3a29636e18e1359307fb74a333fdccb3730d/rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3", size = 1449949 }, + { url = "https://files.pythonhosted.org/packages/12/ae/15c71d68a6df6b8e24595421fdf5bcb305888318e870b7be8d935a9187ee/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d7cec4242d30dd521ef91c0df872e14449d1dffc2a6990ede33943b0dae56c3", size = 1424199 }, + { url = "https://files.pythonhosted.org/packages/dc/9a/765beb9e14d7b30d12e2d6019e8b93747a0bedbc1d0cce13184fa3825426/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e297c09972698c95649e89121e3550cee761ca3640cd005e24aaa2619175464e", size = 5352400 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/49479fe6f06b06cd54d6345ed16de3d1ac659b57730bdbe897df1e059471/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef0f5f03f61b0e5a57b1df7beafd83df993fd5811a09871bad6038d08e526d0d", size = 1652465 }, + { url = "https://files.pythonhosted.org/packages/6f/d8/08823d496b7dd142a7b5d2da04337df6673a14677cfdb72f2604c64ead69/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8cf5f7cd6e4d5eb272baf6a54e182b2c237548d048e2882258336533f3f02b7", size = 1616590 }, + { url = "https://files.pythonhosted.org/packages/38/d4/5cfbc9a997e544f07f301c54d42aac9e0d28d457d543169e4ec859b8ce0d/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9256218ac8f1a957806ec2fb9a6ddfc6c32ea937c0429e88cf16362a20ed8602", size = 3086956 }, + { url = "https://files.pythonhosted.org/packages/25/1e/06d8932a72fa9576095234a15785136407acf8f9a7dbc8136389a3429da1/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1bdd2e6d0c5f9706ef7595773a81ca2b40f3b33fd7f9840b726fb00c6c4eb2e", size = 2494220 }, + { url = "https://files.pythonhosted.org/packages/03/16/5acf15df63119d5ca3d9a54b82807866ff403461811d077201ca351a40c3/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5280be8fd7e2bee5822e254fe0a5763aa0ad57054b85a32a3d9970e9b09bbcbf", size = 7585481 }, + { url = "https://files.pythonhosted.org/packages/e1/cf/ebade4009431ea8e715e59e882477a970834ddaacd1a670095705b86bd0d/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd742c03885db1fce798a1cd87a20f47f144ccf26d75d52feb6f2bae3d57af05", size = 2894842 }, + { url = "https://files.pythonhosted.org/packages/a7/bd/0732632bd3f906bf613229ee1b7cbfba77515db714a0e307becfa8a970ae/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5435fcac94c9ecf0504bf88a8a60c55482c32e18e108d6079a0089c47f3f8cf6", size = 3438517 }, + { url = "https://files.pythonhosted.org/packages/83/89/d3bd47ec9f4b0890f62aea143a1e35f78f3d8329b93d9495b4fa8a3cbfc3/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:93a755266856599be4ab6346273f192acde3102d7aa0735e2f48b456397a041f", size = 4412773 }, + { url = "https://files.pythonhosted.org/packages/b3/57/1a152a07883e672fc117c7f553f5b933f6e43c431ac3fd0e8dae5008f481/rapidfuzz-3.13.0-cp310-cp310-win32.whl", hash = "sha256:3abe6a4e8eb4cfc4cda04dd650a2dc6d2934cbdeda5def7e6fd1c20f6e7d2a0b", size = 1842334 }, + { url = "https://files.pythonhosted.org/packages/a7/68/7248addf95b6ca51fc9d955161072285da3059dd1472b0de773cff910963/rapidfuzz-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8ddb58961401da7d6f55f185512c0d6bd24f529a637078d41dd8ffa5a49c107", size = 1624392 }, + { url = "https://files.pythonhosted.org/packages/68/23/f41c749f2c61ed1ed5575eaf9e73ef9406bfedbf20a3ffa438d15b5bf87e/rapidfuzz-3.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:c523620d14ebd03a8d473c89e05fa1ae152821920c3ff78b839218ff69e19ca3", size = 865584 }, + { url = "https://files.pythonhosted.org/packages/d5/e1/f5d85ae3c53df6f817ca70dbdd37c83f31e64caced5bb867bec6b43d1fdf/rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe5790a36d33a5d0a6a1f802aa42ecae282bf29ac6f7506d8e12510847b82a45", size = 1904437 }, + { url = "https://files.pythonhosted.org/packages/db/d7/ded50603dddc5eb182b7ce547a523ab67b3bf42b89736f93a230a398a445/rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cdb33ee9f8a8e4742c6b268fa6bd739024f34651a06b26913381b1413ebe7590", size = 1383126 }, + { url = "https://files.pythonhosted.org/packages/c4/48/6f795e793babb0120b63a165496d64f989b9438efbeed3357d9a226ce575/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99b76b93f7b495eee7dcb0d6a38fb3ce91e72e99d9f78faa5664a881cb2b7d", size = 1365565 }, + { url = "https://files.pythonhosted.org/packages/f0/50/0062a959a2d72ed17815824e40e2eefdb26f6c51d627389514510a7875f3/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af42f2ede8b596a6aaf6d49fdee3066ca578f4856b85ab5c1e2145de367a12d", size = 5251719 }, + { url = "https://files.pythonhosted.org/packages/e7/02/bd8b70cd98b7a88e1621264778ac830c9daa7745cd63e838bd773b1aeebd/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c0efa73afbc5b265aca0d8a467ae2a3f40d6854cbe1481cb442a62b7bf23c99", size = 2991095 }, + { url = "https://files.pythonhosted.org/packages/9f/8d/632d895cdae8356826184864d74a5f487d40cb79f50a9137510524a1ba86/rapidfuzz-3.13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7ac21489de962a4e2fc1e8f0b0da4aa1adc6ab9512fd845563fecb4b4c52093a", size = 1553888 }, ] [[package]] @@ -2790,114 +2818,139 @@ wheels = [ [[package]] name = "rich" -version = "13.9.4" +version = "14.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] [[package]] name = "rich-toolkit" -version = "0.13.2" +version = "0.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/71cfbf6bf6257ea785d1f030c22468f763eea1b3e5417620f2ba9abd6dca/rich_toolkit-0.13.2.tar.gz", hash = "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3", size = 72288 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/69/e328fb8986814147562b2617f22b06723f60b0c85c85afc0408b9f324a97/rich_toolkit-0.14.3.tar.gz", hash = "sha256:b72a342e52253b912681b027e94226e2deea616494420eec0b09a7219a72a0a5", size = 104469 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/1b/1c2f43af46456050b27810a7a013af8a7e12bc545a0cdc00eb0df55eb769/rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", size = 13566 }, + { url = "https://files.pythonhosted.org/packages/b4/09/e0c7b06657ca1d4317d9e37ea5657a88a20dc3507b2ee6939ace0ff9036e/rich_toolkit-0.14.3-py3-none-any.whl", hash = "sha256:2ec72dcdf1bbb09b6a9286a4eddcd4d43369da3b22fe3f28e5a92143618b8ac6", size = 24258 }, ] [[package]] name = "rpds-py" -version = "0.22.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/80/cce854d0921ff2f0a9fa831ba3ad3c65cee3a46711addf39a2af52df2cfd/rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", size = 26771 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/2a/ead1d09e57449b99dcc190d8d2323e3a167421d8f8fdf0f217c6f6befe47/rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967", size = 359514 }, - { url = "https://files.pythonhosted.org/packages/8f/7e/1254f406b7793b586c68e217a6a24ec79040f85e030fff7e9049069284f4/rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37", size = 349031 }, - { url = "https://files.pythonhosted.org/packages/aa/da/17c6a2c73730d426df53675ff9cc6653ac7a60b6438d03c18e1c822a576a/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24", size = 381485 }, - { url = "https://files.pythonhosted.org/packages/aa/13/2dbacd820466aa2a3c4b747afb18d71209523d353cf865bf8f4796c969ea/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff", size = 386794 }, - { url = "https://files.pythonhosted.org/packages/6d/62/96905d0a35ad4e4bc3c098b2f34b2e7266e211d08635baa690643d2227be/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c", size = 423523 }, - { url = "https://files.pythonhosted.org/packages/eb/1b/d12770f2b6a9fc2c3ec0d810d7d440f6d465ccd8b7f16ae5385952c28b89/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e", size = 446695 }, - { url = "https://files.pythonhosted.org/packages/4d/cf/96f1fd75512a017f8e07408b6d5dbeb492d9ed46bfe0555544294f3681b3/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec", size = 381959 }, - { url = "https://files.pythonhosted.org/packages/ab/f0/d1c5b501c8aea85aeb938b555bfdf7612110a2f8cdc21ae0482c93dd0c24/rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c", size = 410420 }, - { url = "https://files.pythonhosted.org/packages/33/3b/45b6c58fb6aad5a569ae40fb890fc494c6b02203505a5008ee6dc68e65f7/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09", size = 557620 }, - { url = "https://files.pythonhosted.org/packages/83/62/3fdd2d3d47bf0bb9b931c4c73036b4ab3ec77b25e016ae26fab0f02be2af/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00", size = 584202 }, - { url = "https://files.pythonhosted.org/packages/04/f2/5dced98b64874b84ca824292f9cee2e3f30f3bcf231d15a903126684f74d/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf", size = 552787 }, - { url = "https://files.pythonhosted.org/packages/67/13/2273dea1204eda0aea0ef55145da96a9aa28b3f88bb5c70e994f69eda7c3/rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652", size = 220088 }, - { url = "https://files.pythonhosted.org/packages/4e/80/8c8176b67ad7f4a894967a7a4014ba039626d96f1d4874d53e409b58d69f/rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8", size = 231737 }, - { url = "https://files.pythonhosted.org/packages/8b/63/e29f8ee14fcf383574f73b6bbdcbec0fbc2e5fc36b4de44d1ac389b1de62/rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d", size = 360786 }, - { url = "https://files.pythonhosted.org/packages/d3/e0/771ee28b02a24e81c8c0e645796a371350a2bb6672753144f36ae2d2afc9/rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd", size = 350589 }, - { url = "https://files.pythonhosted.org/packages/cf/49/abad4c4a1e6f3adf04785a99c247bfabe55ed868133e2d1881200aa5d381/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493", size = 381848 }, - { url = "https://files.pythonhosted.org/packages/3a/7d/f4bc6d6fbe6af7a0d2b5f2ee77079efef7c8528712745659ec0026888998/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96", size = 387879 }, - { url = "https://files.pythonhosted.org/packages/13/b0/575c797377fdcd26cedbb00a3324232e4cb2c5d121f6e4b0dbf8468b12ef/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123", size = 423916 }, - { url = "https://files.pythonhosted.org/packages/54/78/87157fa39d58f32a68d3326f8a81ad8fb99f49fe2aa7ad9a1b7d544f9478/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad", size = 448410 }, - { url = "https://files.pythonhosted.org/packages/59/69/860f89996065a88be1b6ff2d60e96a02b920a262d8aadab99e7903986597/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9", size = 382841 }, - { url = "https://files.pythonhosted.org/packages/bd/d7/bc144e10d27e3cb350f98df2492a319edd3caaf52ddfe1293f37a9afbfd7/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e", size = 409662 }, - { url = "https://files.pythonhosted.org/packages/14/2a/6bed0b05233c291a94c7e89bc76ffa1c619d4e1979fbfe5d96024020c1fb/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338", size = 558221 }, - { url = "https://files.pythonhosted.org/packages/11/23/cd8f566de444a137bc1ee5795e47069a947e60810ba4152886fe5308e1b7/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566", size = 583780 }, - { url = "https://files.pythonhosted.org/packages/8d/63/79c3602afd14d501f751e615a74a59040328da5ef29ed5754ae80d236b84/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe", size = 553619 }, - { url = "https://files.pythonhosted.org/packages/9f/2e/c5c1689e80298d4e94c75b70faada4c25445739d91b94c211244a3ed7ed1/rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d", size = 233338 }, +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/cbc43b220c9deb536b07fbd598c97d463bbb7afb788851891252fc920742/rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724", size = 377531 }, + { url = "https://files.pythonhosted.org/packages/42/15/cc4b09ef160483e49c3aab3b56f3d375eadf19c87c48718fb0147e86a446/rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b", size = 362273 }, + { url = "https://files.pythonhosted.org/packages/8c/a2/67718a188a88dbd5138d959bed6efe1cc7413a4caa8283bd46477ed0d1ad/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727", size = 388111 }, + { url = "https://files.pythonhosted.org/packages/e5/e6/cbf1d3163405ad5f4a1a6d23f80245f2204d0c743b18525f34982dec7f4d/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964", size = 394447 }, + { url = "https://files.pythonhosted.org/packages/21/bb/4fe220ccc8a549b38b9e9cec66212dc3385a82a5ee9e37b54411cce4c898/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5", size = 448028 }, + { url = "https://files.pythonhosted.org/packages/a5/41/d2d6e0fd774818c4cadb94185d30cf3768de1c2a9e0143fc8bc6ce59389e/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664", size = 447410 }, + { url = "https://files.pythonhosted.org/packages/a7/a7/6d04d438f53d8bb2356bb000bea9cf5c96a9315e405b577117e344cc7404/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc", size = 389531 }, + { url = "https://files.pythonhosted.org/packages/23/be/72e6df39bd7ca5a66799762bf54d8e702483fdad246585af96723109d486/rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0", size = 420099 }, + { url = "https://files.pythonhosted.org/packages/8c/c9/ca100cd4688ee0aa266197a5cb9f685231676dd7d573041ca53787b23f4e/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f", size = 564950 }, + { url = "https://files.pythonhosted.org/packages/05/98/908cd95686d33b3ac8ac2e582d7ae38e2c3aa2c0377bf1f5663bafd1ffb2/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f", size = 591778 }, + { url = "https://files.pythonhosted.org/packages/7b/ac/e143726f1dd3215efcb974b50b03bd08a8a1556b404a0a7872af6d197e57/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875", size = 560421 }, + { url = "https://files.pythonhosted.org/packages/60/28/add1c1d2fcd5aa354f7225d036d4492261759a22d449cff14841ef36a514/rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07", size = 222089 }, + { url = "https://files.pythonhosted.org/packages/b0/ac/81f8066c6de44c507caca488ba336ae30d35d57f61fe10578824d1a70196/rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052", size = 234622 }, + { url = "https://files.pythonhosted.org/packages/99/48/11dae46d0c7f7e156ca0971a83f89c510af0316cd5d42c771b7cef945f0c/rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a", size = 378224 }, + { url = "https://files.pythonhosted.org/packages/33/18/e8398d255369e35d312942f3bb8ecaff013c44968904891be2ab63b3aa94/rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399", size = 363252 }, + { url = "https://files.pythonhosted.org/packages/17/39/dd73ba691f4df3e6834bf982de214086ac3359ab3ac035adfb30041570e3/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098", size = 388871 }, + { url = "https://files.pythonhosted.org/packages/2f/2e/da0530b25cabd0feca2a759b899d2df325069a94281eeea8ac44c6cfeff7/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d", size = 394766 }, + { url = "https://files.pythonhosted.org/packages/4c/ee/dd1c5040a431beb40fad4a5d7868acf343444b0bc43e627c71df2506538b/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e", size = 448712 }, + { url = "https://files.pythonhosted.org/packages/f5/ec/6b93ffbb686be948e4d91ec76f4e6757f8551034b2a8176dd848103a1e34/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1", size = 447150 }, + { url = "https://files.pythonhosted.org/packages/55/d5/a1c23760adad85b432df074ced6f910dd28f222b8c60aeace5aeb9a6654e/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb", size = 390662 }, + { url = "https://files.pythonhosted.org/packages/a5/f3/419cb1f9bfbd3a48c256528c156e00f3349e3edce5ad50cbc141e71f66a5/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44", size = 421351 }, + { url = "https://files.pythonhosted.org/packages/98/8e/62d1a55078e5ede0b3b09f35e751fa35924a34a0d44d7c760743383cd54a/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33", size = 566074 }, + { url = "https://files.pythonhosted.org/packages/fc/69/b7d1003166d78685da032b3c4ff1599fa536a3cfe6e5ce2da87c9c431906/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164", size = 592398 }, + { url = "https://files.pythonhosted.org/packages/ea/a8/1c98bc99338c37faadd28dd667d336df7409d77b4da999506a0b6b1c0aa2/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc", size = 561114 }, + { url = "https://files.pythonhosted.org/packages/2b/41/65c91443685a4c7b5f1dd271beadc4a3e063d57c3269221548dd9416e15c/rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5", size = 235548 }, ] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, +] + +[[package]] +name = "ruff" +version = "0.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, + { url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453 }, + { url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566 }, + { url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020 }, + { url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935 }, + { url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971 }, + { url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631 }, + { url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236 }, + { url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436 }, + { url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759 }, + { url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985 }, + { url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775 }, + { url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307 }, + { url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026 }, + { url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627 }, + { url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340 }, + { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080 }, ] [[package]] name = "s3transfer" -version = "0.11.2" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/45/2323b5928f86fd29f9afdcef4659f68fa73eaa5356912b774227f5cf46b5/s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f", size = 147885 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/9e/73b14aed38ee1f62cd30ab93cd0072dec7fb01f3033d116875ae3e7b8b44/s3transfer-0.12.0.tar.gz", hash = "sha256:8ac58bc1989a3fdb7c7f3ee0918a66b160d038a147c7b5db1500930a607e9a1c", size = 149178 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/ac/e7dc469e49048dc57f62e0c555d2ee3117fa30813d2a1a2962cce3a2a82a/s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc", size = 84151 }, + { url = "https://files.pythonhosted.org/packages/89/64/d2b49620039b82688aeebd510bd62ff4cdcdb86cbf650cc72ae42c5254a3/s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18", size = 84773 }, ] [[package]] name = "safetensors" -version = "0.5.2" +version = "0.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/4f/2ef9ef1766f8c194b01b67a63a444d2e557c8fe1d82faf3ebd85f370a917/safetensors-0.5.2.tar.gz", hash = "sha256:cb4a8d98ba12fa016f4241932b1fc5e702e5143f5374bba0bbcf7ddc1c4cf2b8", size = 66957 } +sdist = { url = "https://files.pythonhosted.org/packages/71/7e/2d5d6ee7b40c0682315367ec7475693d110f512922d582fef1bd4a63adc3/safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965", size = 67210 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/d1/017e31e75e274492a11a456a9e7c171f8f7911fe50735b4ec6ff37221220/safetensors-0.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:45b6092997ceb8aa3801693781a71a99909ab9cc776fbc3fa9322d29b1d3bef2", size = 427067 }, - { url = "https://files.pythonhosted.org/packages/24/84/e9d3ff57ae50dd0028f301c9ee064e5087fe8b00e55696677a0413c377a7/safetensors-0.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6d0d6a8ee2215a440e1296b843edf44fd377b055ba350eaba74655a2fe2c4bae", size = 408856 }, - { url = "https://files.pythonhosted.org/packages/f1/1d/fe95f5dd73db16757b11915e8a5106337663182d0381811c81993e0014a9/safetensors-0.5.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86016d40bcaa3bcc9a56cd74d97e654b5f4f4abe42b038c71e4f00a089c4526c", size = 450088 }, - { url = "https://files.pythonhosted.org/packages/cf/21/e527961b12d5ab528c6e47b92d5f57f33563c28a972750b238b871924e49/safetensors-0.5.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:990833f70a5f9c7d3fc82c94507f03179930ff7d00941c287f73b6fcbf67f19e", size = 458966 }, - { url = "https://files.pythonhosted.org/packages/a5/8b/1a037d7a57f86837c0b41905040369aea7d8ca1ec4b2a77592372b2ec380/safetensors-0.5.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dfa7c2f3fe55db34eba90c29df94bcdac4821043fc391cb5d082d9922013869", size = 509915 }, - { url = "https://files.pythonhosted.org/packages/61/3d/03dd5cfd33839df0ee3f4581a20bd09c40246d169c0e4518f20b21d5f077/safetensors-0.5.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46ff2116150ae70a4e9c490d2ab6b6e1b1b93f25e520e540abe1b81b48560c3a", size = 527664 }, - { url = "https://files.pythonhosted.org/packages/c5/dc/8952caafa9a10a3c0f40fa86bacf3190ae7f55fa5eef87415b97b29cb97f/safetensors-0.5.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab696dfdc060caffb61dbe4066b86419107a24c804a4e373ba59be699ebd8d5", size = 461978 }, - { url = "https://files.pythonhosted.org/packages/60/da/82de1fcf1194e3dbefd4faa92dc98b33c06bed5d67890e0962dd98e18287/safetensors-0.5.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03c937100f38c9ff4c1507abea9928a6a9b02c9c1c9c3609ed4fb2bf413d4975", size = 491253 }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d90e273c25f90c3ba1b0196a972003786f04c39e302fbd6649325b1272bb/safetensors-0.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a00e737948791b94dad83cf0eafc09a02c4d8c2171a239e8c8572fe04e25960e", size = 628644 }, - { url = "https://files.pythonhosted.org/packages/70/3c/acb23e05aa34b4f5edd2e7f393f8e6480fbccd10601ab42cd03a57d4ab5f/safetensors-0.5.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d3a06fae62418ec8e5c635b61a8086032c9e281f16c63c3af46a6efbab33156f", size = 721648 }, - { url = "https://files.pythonhosted.org/packages/71/45/eaa3dba5253a7c6931230dc961641455710ab231f8a89cb3c4c2af70f8c8/safetensors-0.5.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1506e4c2eda1431099cebe9abf6c76853e95d0b7a95addceaa74c6019c65d8cf", size = 659588 }, - { url = "https://files.pythonhosted.org/packages/b0/71/2f9851164f821064d43b481ddbea0149c2d676c4f4e077b178e7eeaa6660/safetensors-0.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c5b5d9da594f638a259fca766046f44c97244cc7ab8bef161b3e80d04becc76", size = 632533 }, - { url = "https://files.pythonhosted.org/packages/00/f1/5680e2ef61d9c61454fad82c344f0e40b8741a9dbd1e31484f0d31a9b1c3/safetensors-0.5.2-cp38-abi3-win32.whl", hash = "sha256:fe55c039d97090d1f85277d402954dd6ad27f63034fa81985a9cc59655ac3ee2", size = 291167 }, - { url = "https://files.pythonhosted.org/packages/86/ca/aa489392ec6fb59223ffce825461e1f811a3affd417121a2088be7a5758b/safetensors-0.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:78abdddd03a406646107f973c7843276e7b64e5e32623529dc17f3d94a20f589", size = 303756 }, + { url = "https://files.pythonhosted.org/packages/18/ae/88f6c49dbd0cc4da0e08610019a3c78a7d390879a919411a410a1876d03a/safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073", size = 436917 }, + { url = "https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7", size = 418419 }, + { url = "https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467", size = 459493 }, + { url = "https://files.pythonhosted.org/packages/df/5c/bf2cae92222513cc23b3ff85c4a1bb2811a2c3583ac0f8e8d502751de934/safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e", size = 472400 }, + { url = "https://files.pythonhosted.org/packages/58/11/7456afb740bd45782d0f4c8e8e1bb9e572f1bf82899fb6ace58af47b4282/safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d", size = 522891 }, + { url = "https://files.pythonhosted.org/packages/57/3d/fe73a9d2ace487e7285f6e157afee2383bd1ddb911b7cb44a55cf812eae3/safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9", size = 537694 }, + { url = "https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a", size = 471642 }, + { url = "https://files.pythonhosted.org/packages/ce/20/1fbe16f9b815f6c5a672f5b760951e20e17e43f67f231428f871909a37f6/safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d", size = 502241 }, + { url = "https://files.pythonhosted.org/packages/5f/18/8e108846b506487aa4629fe4116b27db65c3dde922de2c8e0cc1133f3f29/safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b", size = 638001 }, + { url = "https://files.pythonhosted.org/packages/82/5a/c116111d8291af6c8c8a8b40628fe833b9db97d8141c2a82359d14d9e078/safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff", size = 734013 }, + { url = "https://files.pythonhosted.org/packages/7d/ff/41fcc4d3b7de837963622e8610d998710705bbde9a8a17221d85e5d0baad/safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135", size = 670687 }, + { url = "https://files.pythonhosted.org/packages/40/ad/2b113098e69c985a3d8fbda4b902778eae4a35b7d5188859b4a63d30c161/safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04", size = 643147 }, + { url = "https://files.pythonhosted.org/packages/0a/0c/95aeb51d4246bd9a3242d3d8349c1112b4ee7611a4b40f0c5c93b05f001d/safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace", size = 296677 }, + { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878 }, ] [[package]] name = "scikit-learn" -version = "1.5.2" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, @@ -2905,13 +2958,13 @@ dependencies = [ { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/89/be41419b4bec629a4691183a5eb1796f91252a13a5ffa243fd958cad7e91/scikit_learn-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", size = 12106070 }, - { url = "https://files.pythonhosted.org/packages/bf/e0/3b6d777d375f3b685f433c93384cdb724fb078e1dc8f8ff0950467e56c30/scikit_learn-1.5.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", size = 10971758 }, - { url = "https://files.pythonhosted.org/packages/7b/31/eb7dd56c371640753953277de11356c46a3149bfeebb3d7dcd90b993715a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", size = 12500080 }, - { url = "https://files.pythonhosted.org/packages/4c/1e/a7c7357e704459c7d56a18df4a0bf08669442d1f8878cc0864beccd6306a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8", size = 13347241 }, - { url = "https://files.pythonhosted.org/packages/48/76/154ebda6794faf0b0f3ccb1b5cd9a19f0a63cb9e1f3d2c61b6114002677b/scikit_learn-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", size = 11000477 }, + { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702 }, + { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765 }, + { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991 }, + { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182 }, + { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517 }, ] [[package]] @@ -2995,11 +3048,11 @@ wheels = [ [[package]] name = "setuptools" -version = "75.8.0" +version = "80.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +sdist = { url = "https://files.pythonhosted.org/packages/44/80/97e25f0f1e4067677806084b7382a6ff9979f3d15119375c475c288db9d7/setuptools-80.0.0.tar.gz", hash = "sha256:c40a5b3729d58dd749c0f08f1a07d134fb8a0a3d7f87dc33e7c5e1f762138650", size = 1354221 } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, + { url = "https://files.pythonhosted.org/packages/23/63/5517029d6696ddf2bd378d46f63f479be001c31b462303170a1da57650cb/setuptools-80.0.0-py3-none-any.whl", hash = "sha256:a38f898dcd6e5380f4da4381a87ec90bd0a7eec23d204a5552e80ee3cab6bd27", size = 1240907 }, ] [[package]] @@ -3040,11 +3093,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.6" +version = "2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, ] [[package]] @@ -3057,23 +3110,23 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.37" +version = "2.0.40" 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/3b/20/93ea2518df4d7a14ebe9ace9ab8bb92aaf7df0072b9007644de74172b06c/sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb", size = 9626249 } +sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299 } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/21/aaf0cd2e7ee56e464af7cba38a54f9c1203570181ec5d847711f33c9f520/SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e", size = 2102915 }, - { url = "https://files.pythonhosted.org/packages/fd/01/6615256759515f13bb7d7b49981326f1f4e80ff1bd92dccd53f99dab79ea/SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069", size = 2094095 }, - { url = "https://files.pythonhosted.org/packages/6a/f2/400252bda1bd67da7a35bb2ab84d10a8ad43975d42f15b207a9efb765446/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1", size = 3076482 }, - { url = "https://files.pythonhosted.org/packages/40/c6/e7e8e894c8f065f96ca202cdb00454d60d4962279b3eb5a81b8766dfa836/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84", size = 3084750 }, - { url = "https://files.pythonhosted.org/packages/d6/ee/1cdab04b7760e48273f2592037df156afae044e2e6589157673bd2a830c0/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f", size = 3040575 }, - { url = "https://files.pythonhosted.org/packages/4d/af/2dd456bfd8d4b9750792ceedd828bddf83860f2420545e5effbaf722dae5/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4", size = 3066113 }, - { url = "https://files.pythonhosted.org/packages/dd/d7/ad997559574f94d7bd895a8a63996afef518d07e9eaf5a2a9cbbcb877c16/SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72", size = 2075239 }, - { url = "https://files.pythonhosted.org/packages/d0/82/141fbed705a21af2d825068831da1d80d720945df60c2b97ddc5133b3714/SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636", size = 2099307 }, - { url = "https://files.pythonhosted.org/packages/3b/36/59cc97c365f2f79ac9f3f51446cae56dfd82c4f2dd98497e6be6de20fb91/SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1", size = 1894113 }, + { url = "https://files.pythonhosted.org/packages/87/fa/8e8fd93684b04e65816be864bebf0000fe1602e5452d006f9acc5db14ce5/sqlalchemy-2.0.40-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1ea21bef99c703f44444ad29c2c1b6bd55d202750b6de8e06a955380f4725d7", size = 2112843 }, + { url = "https://files.pythonhosted.org/packages/ba/87/06992f78a9ce545dfd1fea3dd99262bec5221f6f9d2d2066c3e94662529f/sqlalchemy-2.0.40-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afe63b208153f3a7a2d1a5b9df452b0673082588933e54e7c8aac457cf35e758", size = 2104032 }, + { url = "https://files.pythonhosted.org/packages/92/ee/57dc77282e8be22d686bd4681825299aa1069bbe090564868ea270ed5214/sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8aae085ea549a1eddbc9298b113cffb75e514eadbb542133dd2b99b5fb3b6af", size = 3086406 }, + { url = "https://files.pythonhosted.org/packages/94/3f/ceb9ab214b2e42d2e74a9209b3a2f2f073504eee16cddd2df81feeb67c2f/sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea9181284754d37db15156eb7be09c86e16e50fbe77610e9e7bee09291771a1", size = 3094652 }, + { url = "https://files.pythonhosted.org/packages/00/0a/3401232a5b6d91a2df16c1dc39c6504c54575744c2faafa1e5a50de96621/sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5434223b795be5c5ef8244e5ac98056e290d3a99bdcc539b916e282b160dda00", size = 3050503 }, + { url = "https://files.pythonhosted.org/packages/93/c2/ea7171415ab131397f71a2673645c2fe29ebe9a93063d458eb89e42bf051/sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15d08d5ef1b779af6a0909b97be6c1fd4298057504eb6461be88bd1696cb438e", size = 3076011 }, + { url = "https://files.pythonhosted.org/packages/3d/ee/d8e229280d621bed8c51eebf1dd413aa09ca89e309b1fff40d881dd149af/sqlalchemy-2.0.40-cp310-cp310-win32.whl", hash = "sha256:cd2f75598ae70bcfca9117d9e51a3b06fe29edd972fdd7fd57cc97b4dbf3b08a", size = 2085136 }, + { url = "https://files.pythonhosted.org/packages/60/7f/ea1086136bc648cd4713a1e01869f7fc31979d67b3a8f973f5d9ab8de7e1/sqlalchemy-2.0.40-cp310-cp310-win_amd64.whl", hash = "sha256:2cbafc8d39ff1abdfdda96435f38fab141892dc759a2165947d1a8fffa7ef596", size = 2109421 }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 }, ] [[package]] @@ -3111,14 +3164,14 @@ wheels = [ [[package]] name = "starlette" -version = "0.45.3" +version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, ] [[package]] @@ -3132,11 +3185,11 @@ wheels = [ [[package]] name = "tenacity" -version = "9.0.0" +version = "9.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, ] [[package]] @@ -3310,11 +3363,11 @@ wheels = [ [[package]] name = "termcolor" -version = "2.5.0" +version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b6/8e2aaa8aeb570b5cc955cd913b083d96c5447bbe27eaf330dfd7cc8e3329/termcolor-3.0.1.tar.gz", hash = "sha256:a6abd5c6e1284cea2934443ba806e70e5ec8fd2449021be55c280f8a3731b611", size = 12935 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, + { url = "https://files.pythonhosted.org/packages/a6/7e/a574ccd49ad07e8b117407bac361f1e096b01f1b620365daf60ff702c936/termcolor-3.0.1-py3-none-any.whl", hash = "sha256:da1ed4ec8a5dc5b2e17476d859febdb3cccb612be1c36e64511a6f2485c10c69", size = 7157 }, ] [[package]] @@ -3363,11 +3416,11 @@ wheels = [ [[package]] name = "threadpoolctl" -version = "3.5.0" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 }, + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, ] [[package]] @@ -3384,27 +3437,27 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.21.0" +version = "0.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/41/c2be10975ca37f6ec40d7abd7e98a5213bb04f284b869c1a24e6504fd94d/tokenizers-0.21.0.tar.gz", hash = "sha256:ee0894bf311b75b0c03079f33859ae4b2334d675d4e93f5a4132e1eae2834fe4", size = 343021 } +sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/5c/8b09607b37e996dc47e70d6a7b6f4bdd4e4d5ab22fe49d7374565c7fefaf/tokenizers-0.21.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3c4c93eae637e7d2aaae3d376f06085164e1660f89304c0ab2b1d08a406636b2", size = 2647461 }, - { url = "https://files.pythonhosted.org/packages/22/7a/88e58bb297c22633ed1c9d16029316e5b5ac5ee44012164c2edede599a5e/tokenizers-0.21.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f53ea537c925422a2e0e92a24cce96f6bc5046bbef24a1652a5edc8ba975f62e", size = 2563639 }, - { url = "https://files.pythonhosted.org/packages/f7/14/83429177c19364df27d22bc096d4c2e431e0ba43e56c525434f1f9b0fd00/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b177fb54c4702ef611de0c069d9169f0004233890e0c4c5bd5508ae05abf193", size = 2903304 }, - { url = "https://files.pythonhosted.org/packages/7e/db/3433eab42347e0dc5452d8fcc8da03f638c9accffefe5a7c78146666964a/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b43779a269f4629bebb114e19c3fca0223296ae9fea8bb9a7a6c6fb0657ff8e", size = 2804378 }, - { url = "https://files.pythonhosted.org/packages/57/8b/7da5e6f89736c2ade02816b4733983fca1c226b0c42980b1ae9dc8fcf5cc/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aeb255802be90acfd363626753fda0064a8df06031012fe7d52fd9a905eb00e", size = 3095488 }, - { url = "https://files.pythonhosted.org/packages/4d/f6/5ed6711093dc2c04a4e03f6461798b12669bc5a17c8be7cce1240e0b5ce8/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b09dbeb7a8d73ee204a70f94fc06ea0f17dcf0844f16102b9f414f0b7463ba", size = 3121410 }, - { url = "https://files.pythonhosted.org/packages/81/42/07600892d48950c5e80505b81411044a2d969368cdc0d929b1c847bf6697/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:400832c0904f77ce87c40f1a8a27493071282f785724ae62144324f171377273", size = 3388821 }, - { url = "https://files.pythonhosted.org/packages/22/06/69d7ce374747edaf1695a4f61b83570d91cc8bbfc51ccfecf76f56ab4aac/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84ca973b3a96894d1707e189c14a774b701596d579ffc7e69debfc036a61a04", size = 3008868 }, - { url = "https://files.pythonhosted.org/packages/c8/69/54a0aee4d576045b49a0eb8bffdc495634309c823bf886042e6f46b80058/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb7202d231b273c34ec67767378cd04c767e967fda12d4a9e36208a34e2f137e", size = 8975831 }, - { url = "https://files.pythonhosted.org/packages/f7/f3/b776061e4f3ebf2905ba1a25d90380aafd10c02d406437a8ba22d1724d76/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:089d56db6782a73a27fd8abf3ba21779f5b85d4a9f35e3b493c7bbcbbf0d539b", size = 8920746 }, - { url = "https://files.pythonhosted.org/packages/d8/ee/ce83d5ec8b6844ad4c3ecfe3333d58ecc1adc61f0878b323a15355bcab24/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c87ca3dc48b9b1222d984b6b7490355a6fdb411a2d810f6f05977258400ddb74", size = 9161814 }, - { url = "https://files.pythonhosted.org/packages/18/07/3e88e65c0ed28fa93aa0c4d264988428eef3df2764c3126dc83e243cb36f/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4145505a973116f91bc3ac45988a92e618a6f83eb458f49ea0790df94ee243ff", size = 9357138 }, - { url = "https://files.pythonhosted.org/packages/15/b0/dc4572ca61555fc482ebc933f26cb407c6aceb3dc19c301c68184f8cad03/tokenizers-0.21.0-cp39-abi3-win32.whl", hash = "sha256:eb1702c2f27d25d9dd5b389cc1f2f51813e99f8ca30d9e25348db6585a97e24a", size = 2202266 }, - { url = "https://files.pythonhosted.org/packages/44/69/d21eb253fa91622da25585d362a874fa4710be600f0ea9446d8d0217cec1/tokenizers-0.21.0-cp39-abi3-win_amd64.whl", hash = "sha256:87841da5a25a3a5f70c102de371db120f41873b854ba65e52bccd57df5a3780c", size = 2389192 }, + { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767 }, + { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555 }, + { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541 }, + { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058 }, + { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278 }, + { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253 }, + { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225 }, + { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874 }, + { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448 }, + { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877 }, + { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645 }, + { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380 }, + { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506 }, + { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481 }, ] [[package]] @@ -3504,16 +3557,15 @@ wheels = [ [[package]] name = "transformer-smaller-training-vocab" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, { name = "torch" }, { name = "transformers", extra = ["sentencepiece", "torch"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/28/5214ff2a93d9cccc55d0679cb8cbc3b9d52f1f07860c92841b16cfeb5026/transformer_smaller_training_vocab-0.4.0.tar.gz", hash = "sha256:d7360ac084786f66f99ef16d621f34acbb0dce6d9a624525d1f7dc8b6c3a49f7", size = 12141 } +sdist = { url = "https://files.pythonhosted.org/packages/73/df/d0aefd838aaa30e98344a765bc27da96c2d956193d18f51506efbf57723c/transformer_smaller_training_vocab-0.4.1.tar.gz", hash = "sha256:834a804a712ba23cbe410e390791db70d7812b0d7d3bfe1de3efa7b89a85a06c", size = 11745 } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/c8/6a02e88256dc48faf3eae5732a94035f4ea0edd91d8224333693111921ba/transformer_smaller_training_vocab-0.4.0-py3-none-any.whl", hash = "sha256:01cb3d8f4818121172e1591a06c3149bf49bc18d6f6f269eb42d2c4ed155cfcc", size = 14120 }, + { url = "https://files.pythonhosted.org/packages/d8/b3/d1e0536408c097c49e185a0b18794502ba58566742eb2b02102b974989da/transformer_smaller_training_vocab-0.4.1-py3-none-any.whl", hash = "sha256:e19c4c95b594569710b1235f00ff69ddad6401a15554e59657f768dde885bb3a", size = 14066 }, ] [[package]] @@ -3549,7 +3601,7 @@ torch = [ [[package]] name = "typer" -version = "0.15.1" +version = "0.15.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -3557,9 +3609,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } +sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, + { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253 }, ] [[package]] @@ -3573,32 +3625,44 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, ] [[package]] name = "tzdata" -version = "2025.1" +version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] [[package]] name = "tzlocal" -version = "5.2" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "platform_system == 'Windows'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", size = 30201 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/3f/c4c51c55ff8487f2e6d0e618dba917e3c3ee2caae6cf0fbb59c9b1876f2e/tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", size = 17859 }, + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 }, ] [[package]] @@ -3621,25 +3685,25 @@ wheels = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 }, ] [package.optional-dependencies] @@ -3669,43 +3733,43 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.29.1" +version = "20.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 } +sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, + { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 }, ] [[package]] name = "watchfiles" -version = "1.0.4" +version = "1.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 } +sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/02/22fcaed0396730b0d362bc8d1ffb3be2658fd473eecbb2ba84243e157f11/watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08", size = 395212 }, - { url = "https://files.pythonhosted.org/packages/e9/3d/ec5a2369a46edf3ebe092c39d9ae48e8cb6dacbde51c4b4f98936c524269/watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1", size = 384815 }, - { url = "https://files.pythonhosted.org/packages/df/b4/898991cececbe171e67142c31905510203649569d9817848f47c4177ee42/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a", size = 450680 }, - { url = "https://files.pythonhosted.org/packages/58/f7/d4aa3000e812cfb5e5c2c6c0a3ec9d0a46a42489a8727edd160631c4e210/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1", size = 455923 }, - { url = "https://files.pythonhosted.org/packages/dd/95/7e2e4c6aba1b02fb5c76d2f6a450b85215921ec5f8f7ad5efd075369563f/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3", size = 482339 }, - { url = "https://files.pythonhosted.org/packages/bb/67/4265b0fabcc2ef2c9e3e8802ba7908cf718a357ebfb49c72e53787156a48/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2", size = 519908 }, - { url = "https://files.pythonhosted.org/packages/0d/96/b57802d5f8164bdf070befb4fd3dec4edba5a364ec0670965a97eb8098ce/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2", size = 501410 }, - { url = "https://files.pythonhosted.org/packages/8b/18/6db0de4e8911ba14e31853201b40c0fa9fea5ecf3feb86b0ad58f006dfc3/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899", size = 452876 }, - { url = "https://files.pythonhosted.org/packages/df/df/092a961815edf723a38ba2638c49491365943919c3526cc9cf82c42786a6/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff", size = 615353 }, - { url = "https://files.pythonhosted.org/packages/f3/cf/b85fe645de4ff82f3f436c5e9032379fce37c303f6396a18f9726cc34519/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f", size = 613187 }, - { url = "https://files.pythonhosted.org/packages/f6/d4/a9fea27aef4dd69689bc3556718c1157a7accb72aa035ece87c1fa8483b5/watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f", size = 270799 }, - { url = "https://files.pythonhosted.org/packages/df/02/dbe9d4439f15dd4ad0720b6e039bde9d66d1f830331f34c18eb70fa6608e/watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161", size = 284145 }, - { url = "https://files.pythonhosted.org/packages/6f/06/175d5ac6b838fb319008c0cd981d7bf289317c510154d411d3584ca2b67b/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18", size = 396269 }, - { url = "https://files.pythonhosted.org/packages/86/ee/5db93b0b57dc0587abdbac4149296ee73275f615d790a82cb5598af0557f/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817", size = 386010 }, - { url = "https://files.pythonhosted.org/packages/75/61/fe0dc5fedf152bfc085a53711f740701f6bdb8ab6b5c950402b681d4858b/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0", size = 450913 }, - { url = "https://files.pythonhosted.org/packages/9f/dd/3c7731af3baf1a9957afc643d176f94480921a690ec3237c9f9d11301c08/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d", size = 453474 }, + { url = "https://files.pythonhosted.org/packages/af/4d/d02e6ea147bb7fff5fd109c694a95109612f419abed46548a930e7f7afa3/watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40", size = 405632 }, + { url = "https://files.pythonhosted.org/packages/60/31/9ee50e29129d53a9a92ccf1d3992751dc56fc3c8f6ee721be1c7b9c81763/watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb", size = 395734 }, + { url = "https://files.pythonhosted.org/packages/ad/8c/759176c97195306f028024f878e7f1c776bda66ccc5c68fa51e699cf8f1d/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11", size = 455008 }, + { url = "https://files.pythonhosted.org/packages/55/1a/5e977250c795ee79a0229e3b7f5e3a1b664e4e450756a22da84d2f4979fe/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487", size = 459029 }, + { url = "https://files.pythonhosted.org/packages/e6/17/884cf039333605c1d6e296cf5be35fad0836953c3dfd2adb71b72f9dbcd0/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256", size = 488916 }, + { url = "https://files.pythonhosted.org/packages/ef/e0/bcb6e64b45837056c0a40f3a2db3ef51c2ced19fda38484fa7508e00632c/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85", size = 523763 }, + { url = "https://files.pythonhosted.org/packages/24/e9/f67e9199f3bb35c1837447ecf07e9830ec00ff5d35a61e08c2cd67217949/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358", size = 502891 }, + { url = "https://files.pythonhosted.org/packages/23/ed/a6cf815f215632f5c8065e9c41fe872025ffea35aa1f80499f86eae922db/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614", size = 454921 }, + { url = "https://files.pythonhosted.org/packages/92/4c/e14978599b80cde8486ab5a77a821e8a982ae8e2fcb22af7b0886a033ec8/watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f", size = 631422 }, + { url = "https://files.pythonhosted.org/packages/b2/1a/9263e34c3458f7614b657f974f4ee61fd72f58adce8b436e16450e054efd/watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d", size = 625675 }, + { url = "https://files.pythonhosted.org/packages/96/1f/1803a18bd6ab04a0766386a19bcfe64641381a04939efdaa95f0e3b0eb58/watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff", size = 277921 }, + { url = "https://files.pythonhosted.org/packages/c2/3b/29a89de074a7d6e8b4dc67c26e03d73313e4ecf0d6e97e942a65fa7c195e/watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92", size = 291526 }, + { url = "https://files.pythonhosted.org/packages/1a/03/81f9fcc3963b3fc415cd4b0b2b39ee8cc136c42fb10a36acf38745e9d283/watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d", size = 405947 }, + { url = "https://files.pythonhosted.org/packages/54/97/8c4213a852feb64807ec1d380f42d4fc8bfaef896bdbd94318f8fd7f3e4e/watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034", size = 397276 }, + { url = "https://files.pythonhosted.org/packages/78/12/d4464d19860cb9672efa45eec1b08f8472c478ed67dcd30647c51ada7aef/watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965", size = 455550 }, + { url = "https://files.pythonhosted.org/packages/90/fb/b07bcdf1034d8edeaef4c22f3e9e3157d37c5071b5f9492ffdfa4ad4bed7/watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57", size = 455542 }, ] [[package]] @@ -3746,28 +3810,28 @@ wheels = [ [[package]] name = "websockets" -version = "14.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/fa/76607eb7dcec27b2d18d63f60a32e60e2b8629780f343bb83a4dbb9f4350/websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885", size = 163089 }, - { url = "https://files.pythonhosted.org/packages/9e/00/ad2246b5030575b79e7af0721810fdaecaf94c4b2625842ef7a756fa06dd/websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397", size = 160741 }, - { url = "https://files.pythonhosted.org/packages/72/f7/60f10924d333a28a1ff3fcdec85acf226281331bdabe9ad74947e1b7fc0a/websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610", size = 160996 }, - { url = "https://files.pythonhosted.org/packages/63/7c/c655789cf78648c01ac6ecbe2d6c18f91b75bdc263ffee4d08ce628d12f0/websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3", size = 169974 }, - { url = "https://files.pythonhosted.org/packages/fb/5b/013ed8b4611857ac92ac631079c08d9715b388bd1d88ec62e245f87a39df/websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980", size = 168985 }, - { url = "https://files.pythonhosted.org/packages/cd/33/aa3e32fd0df213a5a442310754fe3f89dd87a0b8e5b4e11e0991dd3bcc50/websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8", size = 169297 }, - { url = "https://files.pythonhosted.org/packages/93/17/dae0174883d6399f57853ac44abf5f228eaba86d98d160f390ffabc19b6e/websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7", size = 169677 }, - { url = "https://files.pythonhosted.org/packages/42/e2/0375af7ac00169b98647c804651c515054b34977b6c1354f1458e4116c1e/websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f", size = 169089 }, - { url = "https://files.pythonhosted.org/packages/73/8d/80f71d2a351a44b602859af65261d3dde3a0ce4e76cf9383738a949e0cc3/websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d", size = 169026 }, - { url = "https://files.pythonhosted.org/packages/48/97/173b1fa6052223e52bb4054a141433ad74931d94c575e04b654200b98ca4/websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d", size = 163967 }, - { url = "https://files.pythonhosted.org/packages/c0/5b/2fcf60f38252a4562b28b66077e0d2b48f91fef645d5f78874cd1dec807b/websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2", size = 164413 }, - { url = "https://files.pythonhosted.org/packages/10/3d/91d3d2bb1325cd83e8e2c02d0262c7d4426dc8fa0831ef1aa4d6bf2041af/websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29", size = 160773 }, - { url = "https://files.pythonhosted.org/packages/33/7c/cdedadfef7381939577858b1b5718a4ab073adbb584e429dd9d9dc9bfe16/websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c", size = 161007 }, - { url = "https://files.pythonhosted.org/packages/ca/35/7a20a3c450b27c04e50fbbfc3dfb161ed8e827b2a26ae31c4b59b018b8c6/websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2", size = 162264 }, - { url = "https://files.pythonhosted.org/packages/e8/9c/e3f9600564b0c813f2448375cf28b47dc42c514344faed3a05d71fb527f9/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c", size = 161873 }, - { url = "https://files.pythonhosted.org/packages/3f/37/260f189b16b2b8290d6ae80c9f96d8b34692cf1bb3475df54c38d3deb57d/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a", size = 161818 }, - { url = "https://files.pythonhosted.org/packages/ff/1e/e47dedac8bf7140e59aa6a679e850c4df9610ae844d71b6015263ddea37b/websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3", size = 164465 }, - { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 }, +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080 }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312 }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319 }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631 }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016 }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426 }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360 }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] [[package]] @@ -3793,11 +3857,11 @@ wheels = [ [[package]] name = "widgetsnbextension" -version = "4.0.13" +version = "4.0.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/fc/238c424fd7f4ebb25f8b1da9a934a3ad7c848286732ae04263661eb0fc03/widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6", size = 1164730 } +sdist = { url = "https://files.pythonhosted.org/packages/41/53/2e0253c5efd69c9656b1843892052a31c36d37ad42812b5da45c62191f7e/widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af", size = 1097428 } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/02/88b65cc394961a60c43c70517066b6b679738caf78506a5da7b88ffcb643/widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71", size = 2335872 }, + { url = "https://files.pythonhosted.org/packages/ca/51/5447876806d1088a0f8f71e16542bf350918128d0a69437df26047c8e46f/widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575", size = 2196503 }, ] [[package]] @@ -3840,11 +3904,11 @@ wheels = [ [[package]] name = "xlsxwriter" -version = "3.2.1" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/4f/108b0bada5cfcc47c24ea6181f4c563fbafef50bcc0054089c256b2ae578/XlsxWriter-3.2.1.tar.gz", hash = "sha256:97618759cb264fb6a93397f660cca156ffa9561743b1823dafb60dc4474e1902", size = 202868 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/d1/e026d33dd5d552e5bf3a873dee54dad66b550230df8290d79394f09b2315/xlsxwriter-3.2.3.tar.gz", hash = "sha256:ad6fd41bdcf1b885876b1f6b7087560aecc9ae5a9cc2ba97dcac7ab2e210d3d5", size = 209135 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/30/040af902cb8a9909d320779d8467aa7590bb91477767fd2b7551f4d91bb5/XlsxWriter-3.2.1-py3-none-any.whl", hash = "sha256:7e8f7c60b7a1660ef791d46ab5de78469cb978b991ca841af61f5832d2f9f4fe", size = 162778 }, + { url = "https://files.pythonhosted.org/packages/37/b1/a252d499f2760b314fcf264d2b36fcc4343a1ecdb25492b210cb0db70a68/XlsxWriter-3.2.3-py3-none-any.whl", hash = "sha256:593f8296e8a91790c6d0378ab08b064f34a642b3feb787cf6738236bd0a4860d", size = 169433 }, ] [[package]] @@ -3886,30 +3950,31 @@ wheels = [ [[package]] name = "yarl" -version = "1.18.3" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, - { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, - { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, - { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, - { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, - { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, - { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, - { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, - { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, - { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, - { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, - { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, - { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, - { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, - { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, - { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, - { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/ab/66082639f99d7ef647a86b2ff4ca20f8ae13bd68a6237e6e166b8eb92edf/yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22", size = 145054 }, + { url = "https://files.pythonhosted.org/packages/3d/c2/4e78185c453c3ca02bd11c7907394d0410d26215f9e4b7378648b3522a30/yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62", size = 96811 }, + { url = "https://files.pythonhosted.org/packages/c7/45/91e31dccdcf5b7232dcace78bd51a1bb2d7b4b96c65eece0078b620587d1/yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569", size = 94566 }, + { url = "https://files.pythonhosted.org/packages/c8/21/e0aa650bcee881fb804331faa2c0f9a5d6be7609970b2b6e3cdd414e174b/yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe", size = 327297 }, + { url = "https://files.pythonhosted.org/packages/1a/a4/58f10870f5c17595c5a37da4c6a0b321589b7d7976e10570088d445d0f47/yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195", size = 323578 }, + { url = "https://files.pythonhosted.org/packages/07/df/2506b1382cc0c4bb0d22a535dc3e7ccd53da9a59b411079013a7904ac35c/yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10", size = 343212 }, + { url = "https://files.pythonhosted.org/packages/ba/4a/d1c901d0e2158ad06bb0b9a92473e32d992f98673b93c8a06293e091bab0/yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634", size = 337956 }, + { url = "https://files.pythonhosted.org/packages/8b/fd/10fcf7d86f49b1a11096d6846257485ef32e3d3d322e8a7fdea5b127880c/yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2", size = 333889 }, + { url = "https://files.pythonhosted.org/packages/e2/cd/bae926a25154ba31c5fd15f2aa6e50a545c840e08d85e2e2e0807197946b/yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a", size = 322282 }, + { url = "https://files.pythonhosted.org/packages/e2/c6/c3ac3597dfde746c63c637c5422cf3954ebf622a8de7f09892d20a68900d/yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867", size = 336270 }, + { url = "https://files.pythonhosted.org/packages/dd/42/417fd7b8da5846def29712370ea8916a4be2553de42a2c969815153717be/yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995", size = 335500 }, + { url = "https://files.pythonhosted.org/packages/37/aa/c2339683f8f05f4be16831b6ad58d04406cf1c7730e48a12f755da9f5ac5/yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487", size = 339672 }, + { url = "https://files.pythonhosted.org/packages/be/12/ab6c4df95f00d7bc9502bf07a92d5354f11d9d3cb855222a6a8d2bd6e8da/yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2", size = 351840 }, + { url = "https://files.pythonhosted.org/packages/83/3c/08d58c51bbd3899be3e7e83cd7a691fdcf3b9f78b8699d663ecc2c090ab7/yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61", size = 359550 }, + { url = "https://files.pythonhosted.org/packages/8a/15/de7906c506f85fb476f0edac4bd74569f49e5ffdcf98e246a0313bf593b9/yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19", size = 351108 }, + { url = "https://files.pythonhosted.org/packages/25/04/c6754f5ae2cdf057ac094ac01137c17875b629b1c29ed75354626a755375/yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d", size = 86733 }, + { url = "https://files.pythonhosted.org/packages/db/1f/5c1952f3d983ac3f5fb079b5b13b62728f8a73fd27d03e1cef7e476addff/yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076", size = 92916 }, + { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124 }, ] From e97dd6fb3b22ed4c544538bd7b794d6f143f2da4 Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Sun, 25 May 2025 05:41:29 +0000 Subject: [PATCH 07/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix=20API=20endpoint?= =?UTF-8?q?=20for=20document=20extraction,=20remove=20unused=20logo=20file?= =?UTF-8?q?,=20and=20update=20aymurai=20package=20version=20in=20lock=20fi?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/Dockerfile | 1 + .devcontainer/devcontainer.json | 26 +- .vscode/settings.json | 45 +++ aymurai/api/core.py | 17 +- .../endpoints/routers/anonymizer/__init__.py | 3 +- .../compilation.py} | 9 +- .../api/endpoints/routers/anonymizer/core.py | 67 ---- .../endpoints/routers/anonymizer/database.py | 34 -- .../endpoints/routers/anonymizer/document.py | 175 ---------- .../endpoints/routers/anonymizer/paragraph.py | 199 ------------ .../endpoints/routers/database/__init__.py | 2 + .../routers/database/anonymization.py | 108 ------- .../endpoints/routers/database/datapublic.py | 100 ------ .../api/endpoints/routers/database/dump.py | 34 ++ .../endpoints/routers/datapublic/__init__.py | 2 + .../routers/datapublic/datapublic.py | 161 --------- .../endpoints/routers/datapublic/dataset.py | 148 +++++---- .../routers/datapublic/validation.py | 80 +++++ .../endpoints/routers/document/__init__.py | 2 +- .../api/endpoints/routers/document/convert.py | 24 +- .../api/endpoints/routers/document/extract.py | 6 +- .../api/endpoints/routers/document/predict.py | 50 +++ .../api/endpoints/routers/document/search.py | 154 +++++++++ .../endpoints/routers/paragraph/predict.py | 12 +- aymurai/api/exceptions.py | 16 - aymurai/api/main.py | 25 +- aymurai/api/startup/database.py | 6 +- .../database/meta/anonymization/__init__.py | 0 .../database/meta/anonymization/document.py | 45 --- .../meta/anonymization/document_paragraph.py | 21 -- .../database/meta/anonymization/paragraph.py | 67 ---- aymurai/database/meta/datapublic.py | 44 +++ aymurai/database/meta/datapublic/__init__.py | 0 aymurai/database/meta/datapublic/document.py | 54 ---- .../meta/datapublic/document_paragraph.py | 21 -- aymurai/database/meta/datapublic/paragraph.py | 67 ---- aymurai/database/meta/document.py | 15 +- aymurai/database/meta/prediction.py | 9 + aymurai/database/schema.py | 28 +- ...ase.py => 86bdf2a39c7e_create_database.py} | 111 ++----- aymurai/meta/entities.py | 14 +- aymurai/settings.py | 6 +- .../05-entity-grouping/entitty-groups.ipynb | 305 +++++++++--------- .../pdf-support/04-anonymize-pdf.ipynb | 64 ++-- .../pdf-support/05-datapublic.ipynb | 2 +- resources/api/static/logo256-text.ico | Bin 1711 -> 0 bytes uv.lock | 2 +- 47 files changed, 791 insertions(+), 1590 deletions(-) create mode 100644 .vscode/settings.json rename aymurai/api/endpoints/routers/{document/anonymizer.py => anonymizer/compilation.py} (95%) delete mode 100644 aymurai/api/endpoints/routers/anonymizer/core.py delete mode 100644 aymurai/api/endpoints/routers/anonymizer/database.py delete mode 100644 aymurai/api/endpoints/routers/anonymizer/document.py delete mode 100644 aymurai/api/endpoints/routers/anonymizer/paragraph.py delete mode 100644 aymurai/api/endpoints/routers/database/anonymization.py delete mode 100644 aymurai/api/endpoints/routers/database/datapublic.py create mode 100644 aymurai/api/endpoints/routers/database/dump.py delete mode 100644 aymurai/api/endpoints/routers/datapublic/datapublic.py create mode 100644 aymurai/api/endpoints/routers/datapublic/validation.py create mode 100644 aymurai/api/endpoints/routers/document/predict.py create mode 100644 aymurai/api/endpoints/routers/document/search.py delete mode 100644 aymurai/api/exceptions.py delete mode 100644 aymurai/database/meta/anonymization/__init__.py delete mode 100644 aymurai/database/meta/anonymization/document.py delete mode 100644 aymurai/database/meta/anonymization/document_paragraph.py delete mode 100644 aymurai/database/meta/anonymization/paragraph.py create mode 100644 aymurai/database/meta/datapublic.py delete mode 100644 aymurai/database/meta/datapublic/__init__.py delete mode 100644 aymurai/database/meta/datapublic/document.py delete mode 100644 aymurai/database/meta/datapublic/document_paragraph.py delete mode 100644 aymurai/database/meta/datapublic/paragraph.py rename aymurai/database/versions/{d6c141e03875_create_database.py => 86bdf2a39c7e_create_database.py} (54%) delete mode 100644 resources/api/static/logo256-text.ico diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2cad5fc..6fbb8c5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -110,3 +110,4 @@ WORKDIR /workspace COPY .devcontainer/entrypoint.sh /home/$DEVCONTAINER_USER/ ENV UV_CACHE_DIR=/resources/cache/uv +ENV ALAMBIC_CONFIG=/workspace/aymurai/alambic.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2ead995..74634a0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,27 +11,6 @@ "workspaceFolder": "/workspace", "customizations": { "vscode": { - "settings": { - "extensions.verifySignature": false, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "files.watcherExclude": { - "**/.git/objects/**": true, - "**/.git/subtree-cache/**": true - }, - "editor.formatOnSave": true, - "notebook.formatOnSave.enabled": true, - "notebook.output.scrolling": true, - "notebook.codeActionsOnSave": { - "notebook.source.organizeImports": "explicit" - }, - "[python]": { - "editor.defaultFormatter": "charliermarsh.ruff", - "editor.codeActionsOnSave": { - "source.fixAll": "never" - } - } - }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "ms-python.python", @@ -40,14 +19,13 @@ "charliermarsh.ruff", "ms-toolsai.jupyter", "GitHub.vscode-pull-request-github", - "njpwerner.autodocstring", "GrapeCity.gc-excelviewer", "tamasfe.even-better-toml", "redhat.vscode-xml", - "cweijan.vscode-database-client2", "christian-kohler.path-intellisense", "github.vscode-github-actions", - "seatonjiang.gitmoji-vscode" + "seatonjiang.gitmoji-vscode", + "dbcode.dbcode" ] } }, diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0825654 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,45 @@ +{ + "python.analysis.typeCheckingMode": "off", + "extensions.verifySignature": false, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true + }, + "python.analysis.exclude": [ + "**/node_modules", + "**/__pycache__", + "**/build", + ".git", + ".venv/**" + ], + "editor.formatOnSave": true, + "notebook.formatOnSave.enabled": true, + "notebook.output.scrolling": true, + "notebook.codeActionsOnSave": { + "notebook.source.organizeImports": "explicit" + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "never", + "source.organizeImports.ruff": "always" + } + }, + "ruff.format.preview": true, + "ruff.organizeImports": true, + "dbcode.connections": [ + { + "connectionId": "1oe_iPSN7LlyCERrXeOdw", + "name": "aymurai", + "driver": "sqlite", + "socket": "workspace/resources/cache/sqlite/database.db", + "readOnly": false, + "driverOptions": { + "extensions": [] + }, + "connectionType": "socket" + } + ] +} \ No newline at end of file diff --git a/aymurai/api/core.py b/aymurai/api/core.py index c527709..80bf41a 100644 --- a/aymurai/api/core.py +++ b/aymurai/api/core.py @@ -1,11 +1,8 @@ from fastapi.routing import APIRouter -from .endpoints.routers import document, paragraph +from .endpoints.routers import anonymizer, database, datapublic, document, paragraph from .endpoints.routers.server import stats -# from .endpoints.routers.datapublic import datapublic - - router = APIRouter() @@ -15,7 +12,17 @@ # Document router.include_router(document.extract.router, prefix="/document", tags=["document"]) router.include_router(document.convert.router, prefix="/document", tags=["document"]) -router.include_router(document.anonymizer.router, tags=["document"]) +router.include_router(document.predict.router, tags=["document"]) # Paragraph router.include_router(paragraph.predict.router, tags=["paragraph"]) + +# Datapublic +router.include_router(datapublic.validation.router, tags=["datapublic"]) +router.include_router(datapublic.dataset.router, tags=["datapublic"]) + +# Anonymizer +router.include_router(anonymizer.compilation.router, tags=["anonymizer"]) + +# Database +router.include_router(database.dump.router, tags=["database"]) diff --git a/aymurai/api/endpoints/routers/anonymizer/__init__.py b/aymurai/api/endpoints/routers/anonymizer/__init__.py index bbc7b81..8e3c091 100644 --- a/aymurai/api/endpoints/routers/anonymizer/__init__.py +++ b/aymurai/api/endpoints/routers/anonymizer/__init__.py @@ -1 +1,2 @@ -from . import paragraph +# ruff: noqa: F401 +from . import compilation diff --git a/aymurai/api/endpoints/routers/document/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/compilation.py similarity index 95% rename from aymurai/api/endpoints/routers/document/anonymizer.py rename to aymurai/api/endpoints/routers/anonymizer/compilation.py index 0387f85..f99623b 100644 --- a/aymurai/api/endpoints/routers/document/anonymizer.py +++ b/aymurai/api/endpoints/routers/anonymizer/compilation.py @@ -2,7 +2,7 @@ import subprocess import tempfile -from fastapi import Depends +from fastapi import Depends, HTTPException from fastapi.responses import FileResponse from fastapi.routing import APIRouter from pydantic import UUID4, UUID5 @@ -25,7 +25,7 @@ # MARK: Document Compilation @router.get( - f"/document/{{document_id}}/pipeline/{ModelType.ANONYMIZATION}/compile", + f"/pipeline/{ModelType.ANONYMIZATION}/document/{{document_id}}/compile", response_class=FileResponse, ) async def anonymizer_compile_document( @@ -47,7 +47,10 @@ async def anonymizer_compile_document( # ———————— Sanity check ——————————————————————————————————————————————————————————— # Check if the document exists if not document: - raise ValueError(f"Document not found: {document_id}") + raise HTTPException( + status_code=404, + detail=f"Document not found: {document_id}", + ) # ———————— Get annotations ———————————————————————————————————————————————————————— annotations = read_document_prediction_paragraphs( diff --git a/aymurai/api/endpoints/routers/anonymizer/core.py b/aymurai/api/endpoints/routers/anonymizer/core.py deleted file mode 100644 index 8d23d65..0000000 --- a/aymurai/api/endpoints/routers/anonymizer/core.py +++ /dev/null @@ -1,67 +0,0 @@ -import os -from functools import cache - -from fastapi import Depends -from sqlmodel import Session - -from aymurai.api.utils import load_pipeline -from aymurai.database.crud.model import register_model -from aymurai.database.schema import Model -from aymurai.database.session import get_session -from aymurai.logger import get_logger -from aymurai.pipeline.pipeline import AymurAIPipeline -from aymurai.settings import settings - -logger = get_logger(__name__) - -RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH - - -@cache -def get_anonimization_model(session: Session = Depends(get_session)) -> Model: - """ - Get the model from the database. - - Args: - session (Session): Database session dependency. - - Returns: - Model: The model from the database. - """ - return register_model( - model_name="flair-anonymizer", - app_version=settings.APP_VERSION, - session=session, - ) - - -@cache -def get_datapublic_model(session: Session = Depends(get_session)) -> Model: - """ - Get the model from the database. - - Args: - session (Session): Database session dependency. - - Returns: - Model: The model from the database. - """ - return register_model( - model_name="datapublic", - app_version=settings.APP_VERSION, - session=session, - ) - - -@cache -def load_anonymization_pipeline() -> AymurAIPipeline: - return load_pipeline( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") - ) - - -@cache -def load_datapublic_pipeline() -> AymurAIPipeline: - return load_pipeline( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "full-paragraph") - ) diff --git a/aymurai/api/endpoints/routers/anonymizer/database.py b/aymurai/api/endpoints/routers/anonymizer/database.py deleted file mode 100644 index 50bc19c..0000000 --- a/aymurai/api/endpoints/routers/anonymizer/database.py +++ /dev/null @@ -1,34 +0,0 @@ -from pydantic import UUID5 -from fastapi import Depends -from sqlmodel import Session -from fastapi.routing import APIRouter - -from aymurai.logger import get_logger -from aymurai.database.session import get_session - -logger = get_logger(__name__) - - -router = APIRouter() - - -@router.get("/document/list") -async def anonymizer_database_document_list( - session: Session = Depends(get_session), -): - pass - - -@router.get("/document/dump") -async def anonymizer_database_documents_dump( - session: Session = Depends(get_session), -): - pass - - -@router.get("/document/{document_id}") -async def anonymizer_database_document( - document_id: UUID5, - session: Session = Depends(get_session), -): - pass diff --git a/aymurai/api/endpoints/routers/anonymizer/document.py b/aymurai/api/endpoints/routers/anonymizer/document.py deleted file mode 100644 index 434dbfe..0000000 --- a/aymurai/api/endpoints/routers/anonymizer/document.py +++ /dev/null @@ -1,175 +0,0 @@ -import os -import subprocess -import tempfile -from functools import cache -from threading import Lock - -import torch -from fastapi import Body, Depends, Query, HTTPException -from fastapi.responses import FileResponse -from fastapi.routing import APIRouter -from pydantic import UUID4 -from sqlmodel import Session -from starlette.background import BackgroundTask - -from aymurai.api.utils import load_pipeline -from aymurai.database.crud.model import register_model -from aymurai.database.crud.prediction import read_prediction_by_text, read_validation -from aymurai.database.schema import ( - Document, - Model, - Prediction, - PredictionCreate, - Paragraph, -) -from aymurai.database.session import get_session -from aymurai.database.utils import text_to_uuid -from aymurai.logger import get_logger -from aymurai.meta.api_interfaces import ( - DocLabel, - DocumentInformation, - TextRequest, -) -from aymurai.settings import settings -from aymurai.text.anonymization import DocAnonymizer -from aymurai.utils.misc import get_element - -logger = get_logger(__name__) - - -RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads = 100 # FIXME: polemic ? -pipeline_lock = Lock() - - -router = APIRouter() - - -@cache -def get_model(session: Session = Depends(get_session)) -> Model: - """ - Get the model from the database. - - Args: - session (Session): Database session dependency. - - Returns: - Model: The model from the database. - """ - return register_model( - model_name="flair-anonymizer", - app_version=settings.APP_VERSION, - session=session, - ) - - -# MARK: Document Compilation -@router.post("/document/{document_id}/compile", response_model=FileResponse) -async def anonymizer_compile_document( - document_id: UUID4, - session: Session = Depends(get_session), - model: Model = Depends(get_model), -) -> FileResponse: - """ - Compile Anonymized document from original file and annotations - - Args: - file (UploadFile): Original file. - annotations (str, optional): JSON with document annotations. - - Returns: - FileResponse: Anonymized document - """ - document = session.get(Document, document_id) - - # ———————— Sanity check —————————————————————————————————————————————————————————— - # Check if the document exists - if not document: - raise ValueError(f"Document not found: {document_id}") - - #################################################################################### - # Create a temporary file - #################################################################################### - - filename = document.name - logger.info(f"Document found: {document_id} ({filename})") - - # Create a temporary file - _, suffix = os.path.splitext(filename) - suffix = suffix if suffix == ".docx" else ".txt" - tmp_dir = tempfile.gettempdir() - - # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed - # This is necessary on Windows, as the file is locked by the file object and cannot be deleted - with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, dir=tmp_dir) as file: - tmp_filename = file.name - logger.info(f"saving temp file on local storage => {tmp_filename}") - file.write(document.data) - file.flush() - file.close() - - logger.info(f"saved temp file on local storage => {tmp_filename}") - - #################################################################################### - # Anonymize the document - #################################################################################### - doc_anonymizer = DocAnonymizer() - - if suffix == ".docx": - item = {"path": tmp_filename} - doc_anonymizer( - item, - [document_information.model_dump() for document_information in annotations], - tmp_dir, - ) - logger.info(f"saved temp file on local storage => {tmp_filename}") - - else: - # Export as raw document - anonymized_doc = [ - doc_anonymizer.replace_labels_in_text(document_information.model_dump()) - .replace("<", "<") - .replace(">", ">") - for document_information in annotations - ] - with open(tmp_filename, "w") as f: - f.write("\n".join(anonymized_doc)) - - # Convert to ODT - cmd = [ - settings.LIBREOFFICE_BIN, - "--headless", - "--convert-to", - "odt", - "--outdir", - tmp_dir, - tmp_filename, - ] - - logger.info(f"Executing: {' '.join(cmd)}") - - try: - output = subprocess.check_output( - cmd, shell=False, encoding="utf-8", errors="ignore" - ) - logger.info(f"LibreOffice output: {output}") - except subprocess.CalledProcessError as e: - raise RuntimeError( - f"LibreOffice conversion failed: {e.output.decode('utf-8', errors='ignore')}" - ) - - odt = tmp_filename.replace(suffix, ".odt") - logger.info(f"Expected output file path: {odt}") - - if not os.path.exists(odt): - raise RuntimeError(f"File at path {odt} does not exist.") - - # Ensure the temporary file is deleted - os.remove(tmp_filename) - - return FileResponse( - odt, - background=BackgroundTask(os.remove, odt), - media_type="application/octet-stream", - filename=f"{os.path.splitext(filename)[0]}.odt", - ) diff --git a/aymurai/api/endpoints/routers/anonymizer/paragraph.py b/aymurai/api/endpoints/routers/anonymizer/paragraph.py deleted file mode 100644 index de827c6..0000000 --- a/aymurai/api/endpoints/routers/anonymizer/paragraph.py +++ /dev/null @@ -1,199 +0,0 @@ -import os -from threading import Lock - -import torch -from fastapi import Depends, Query, HTTPException -from fastapi.routing import APIRouter -from pydantic import UUID4 -from sqlmodel import Session - -from aymurai.api.utils import load_pipeline -from aymurai.api.endpoints.routers.anonymizer.core import get_anonimization_model -from aymurai.database.schema import ( - Document, - Model, - Prediction, - PredictionCreate, - Paragraph, -) -from aymurai.database.crud.prediction import read_prediction_by_text -from aymurai.database.session import get_session -from aymurai.logger import get_logger -from aymurai.meta.api_interfaces import ( - DocLabel, - DocumentInformation, -) -from aymurai.settings import settings -from aymurai.utils.misc import get_element - -logger = get_logger(__name__) - - -RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads(100) # FIXME: polemic ? -pipeline_lock = Lock() - - -router = APIRouter() - - -# MARK: Paragraph Predict -@router.post("/paragraph/{paragraph_id}", response_model=DocumentInformation) -async def anonymizer_paragraph_predict( - paragraph_id: UUID4, - use_cache: bool = Query( - True, description="Use cache to store or retrive predictions" - ), - session: Session = Depends(get_session), - model: Model = Depends(get_anonimization_model), -) -> DocumentInformation: - """ - Endpoint to predict anonymization for a given paragraph of text. - - Args: - text_request (TextRequest): The request body containing the text to be anonymized. - use_cache (bool): Flag to determine whether to use cache for storing or retrieving predictions. - session (Session): Database session dependency. - - Returns: - DocumentInformation: The anonymized document information including the text and labels. - """ - - paragraph = session.get(Paragraph, paragraph_id) - if not paragraph: - raise HTTPException( - status_code=404, - detail=f"Paragraph not found: {paragraph_id}", - ) - input_text = paragraph.text - - logger.info(f"Running prediction: {model.name} ({model.version})") - - logger.info(f"Checking cache (use cache: {use_cache})") - - # —————— Check cache ————————————————————————————————————————————————————————————— - cached_pred = read_prediction_by_text( - text=input_text, model_id=model.id, session=session - ) - if cached_pred and use_cache: - logger.info(f"cache loaded from prediction: {cached_pred.id}") - logger.debug(f"{cached_pred}") - - labels = cached_pred.validation or cached_pred.prediction - - return DocumentInformation(document=cached_pred.input, labels=labels) - - # ——————— Run prediction ————————————————————————————————————————————————————————— - logger.info("Running prediction") - item = [{"path": "empty", "data": {"doc.text": input_text}}] - pipeline = load_pipeline( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") - ) - - with pipeline_lock: - processed = pipeline.preprocess(item) - processed = pipeline.predict_single(processed[0]) - processed = pipeline.postprocess([processed]) - - output_text = get_element(processed[0], ["data", "doc.text"]) or "" - output_labels = get_element(processed[0], ["predictions", "entities"]) or [] - - # ———————— Sanity check —————————————————————————————————————————————————————————— - if input_text != output_text: - logger.critical( - f"Input and output text do not match: {input_text} != {output_text}" - ) - if settings.ERROR_HANDLER == "raise": - raise ValueError("Input and output text do not match") - - # Run validation on the output labels - output_labels = [ - DocLabel(**label).model_dump(mode="json") for label in output_labels - ] - - # ——————— Save to cache —————————————————————————————————————————————————————————— - if cached_pred: - logger.info(f"saving in cache: {cached_pred.id}") - - logger.warning(f"Prediction already exists: {cached_pred}. Updating.") - cached_pred.input = output_text - cached_pred.prediction = output_labels - else: - pred = PredictionCreate( - input=output_text, - prediction=output_labels, - fk_model=model.id, - fk_paragraph=paragraph.id, - ).compile() - - session.add(cached_pred if cached_pred else pred) - session.commit() - session.refresh(pred) - - return DocumentInformation(document=pred.input, labels=pred.prediction) - - -# MARK: Paragraph Validate -@router.post("/paragraph/{paragraph_id}/validate") -async def anonymizer_save_document_validation( - paragraph_id: UUID4, - annotations: list[DocumentInformation], - session: Session = Depends(get_session), - model: Model = Depends(get_anonimization_model), -) -> list[DocLabel] | None: - document = session.get(Document, paragraph_id) - - # ———————— Sanity check —————————————————————————————————————————————————————————— - # Check if the document exists - if not document: - raise ValueError(f"Document not found: {paragraph_id}") - - # Check length of annotations and document match - if len(annotations) != len(document.paragraphs): - raise ValueError( - f"The number of annotated paragraphs ({len(annotations)}) does not" - f" match the number of paragraphs in the document" - f" ({len(document.paragraphs)})" - ) - - # Check if the annotated paragraphs match the document paragraphs one by one - for i, (annot_paragraph, doc_paragraph) in enumerate( - zip(annotations, document.paragraphs) - ): - if annot_paragraph.document != doc_paragraph.text: - msg = ( - f"The annotated paragraph ({annot_paragraph.document}) does not match" - f" the document paragraph ({doc_paragraph.text})" - ) - if settings.ERROR_HANDLER == "raise": - raise ValueError(msg) - logger.warning(msg) - - # ——————— Updating validations ——————————————————————————————————————————————————— - predictions = [] - for paragraph in annotations: - prediction = read_prediction_by_text( - text=paragraph.document, model_id=model.id, session=session - ) - if prediction: - prediction.validation = paragraph.labels - predictions.append(prediction) - else: - logger.warning("Prediction not found.") - if settings.ERROR_HANDLER == "raise": - raise ValueError( - f"Prediction not found for paragraph: `{paragraph.document}`" - ) - prediction = Prediction( - input=paragraph.document, - fk_model=model.id, - validation=paragraph.labels, - ) - predictions.append(prediction) - - session.add_all(predictions) - session.commit() - for prediction in predictions: - session.refresh(prediction) - - return diff --git a/aymurai/api/endpoints/routers/database/__init__.py b/aymurai/api/endpoints/routers/database/__init__.py index e69de29..70e646a 100644 --- a/aymurai/api/endpoints/routers/database/__init__.py +++ b/aymurai/api/endpoints/routers/database/__init__.py @@ -0,0 +1,2 @@ +# ruff: noqa: F401 +from . import dump diff --git a/aymurai/api/endpoints/routers/database/anonymization.py b/aymurai/api/endpoints/routers/database/anonymization.py deleted file mode 100644 index 2b1b62a..0000000 --- a/aymurai/api/endpoints/routers/database/anonymization.py +++ /dev/null @@ -1,108 +0,0 @@ -from pydantic import UUID5 -from fastapi import Body, Depends -from sqlmodel import Session, select -from fastapi.routing import APIRouter, HTTPException - -from aymurai.database.utils import text_to_uuid -from aymurai.database.session import get_session -from aymurai.meta.api_interfaces import TextRequest, SuccessResponse -from aymurai.database.schema import ( - AnonymizationParagraph, - AnonymizationParagraphRead, - AnonymizationParagraphCreate, - AnonymizationParagraphUpdate, -) -from aymurai.database.crud.anonymization.paragraph import ( - anonymization_paragraph_read, - anonymization_paragraph_create, - anonymization_paragraph_delete, - anonymization_paragraph_update, -) - -router = APIRouter() - - -@router.post("/paragraph", response_model=AnonymizationParagraphRead) -async def api_paragraph_create( - paragraph_in: AnonymizationParagraphCreate, - session: Session = Depends(get_session), -): - """ - Add a new paragraph to the database - """ - - paragraph = anonymization_paragraph_create(paragraph_in, session) - return paragraph - - -@router.get("/paragraph", response_model=AnonymizationParagraphRead | None) -async def paragraph_read_by_text( - data: TextRequest, - session: Session = Depends(get_session), -) -> AnonymizationParagraphRead | None: - paragraph_id = text_to_uuid(data.text) - - return anonymization_paragraph_read(paragraph_id, session) - - -@router.post("/paragraph/text_to_uuid") -async def paragraph_get_uuid(text: str = Body(...)) -> str: - return str(text_to_uuid(text)) - - -@router.get("/paragraph/{paragraph_id}", response_model=AnonymizationParagraphRead) -async def api_paragraph_read( - paragraph_id: UUID5, - session: Session = Depends(get_session), -) -> AnonymizationParagraphRead: - statement = select(AnonymizationParagraph).where( - AnonymizationParagraph.id == paragraph_id - ) - data = session.exec(statement).first() - return data - - -@router.put("/paragraph/{paragraph_id}", response_model=AnonymizationParagraphRead) -async def api_paragraph_update( - paragraph_id: UUID5, - data: AnonymizationParagraphUpdate, - session: Session = Depends(get_session), -) -> AnonymizationParagraphRead: - try: - paragraph = anonymization_paragraph_update(paragraph_id, data, session) - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - return paragraph - - -@router.delete("/paragraph/{paragraph_id}", response_model=SuccessResponse) -async def api_paragraph_delete( - paragraph_id: UUID5, - session: Session = Depends(get_session), -): - try: - anonymization_paragraph_delete(paragraph_id, session) - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - - return SuccessResponse(id=paragraph_id, msg="Item deleted") - - -# @router.post("/document") -# async def document_add(): -# pass - - -# @router.get("/document/{document_id}") -# async def document_get(): -# pass - - -# @router.put("/document/{document_id}") -# async def document_update(): -# pass - - -# @router.delete("/document/{document_id}") -# async def document_delete(): -# pass diff --git a/aymurai/api/endpoints/routers/database/datapublic.py b/aymurai/api/endpoints/routers/database/datapublic.py deleted file mode 100644 index 0b58681..0000000 --- a/aymurai/api/endpoints/routers/database/datapublic.py +++ /dev/null @@ -1,100 +0,0 @@ -import tempfile - -import pandas as pd -from fastapi import Depends -from sqlmodel import Session, select -from fastapi.routing import APIRouter -from fastapi.responses import FileResponse - -from aymurai.database.session import get_session -from aymurai.meta.api_interfaces import SuccessResponse -from aymurai.api.endpoints.meta.database import ExportFormat -from aymurai.database.schema import ( - DataPublicDataset, - DataPublicDatasetRead, - DataPublicDatasetCreate, - DataPublicDatasetUpdate, -) - -router = APIRouter() - - -@router.get("/export") -async def database_export( - format: ExportFormat = ExportFormat.CSV, - session: Session = Depends(get_session), -) -> FileResponse: - statement = select(DataPublicDataset) - data = pd.read_sql(statement, session.bind) - - with tempfile.NamedTemporaryFile(delete=False) as temp: - if format == ExportFormat.CSV: - data.to_csv(temp.name, index=False) - extension = "csv" - elif format == ExportFormat.JSON: - data.to_json(temp.name, orient="records") - extension = "json" - else: - raise ValueError("Invalid export format") - - return FileResponse( - path=temp.name, - filename=f"datapublic.{extension}", - media_type="application/octet-stream", - ) - - -@router.post("/items/add", response_model=SuccessResponse) -async def item_add( - data: DataPublicDatasetCreate, - session: Session = Depends(get_session), -): - item = DataPublicDataset.model_validate(data) - - session.add(item) - session.commit() - session.refresh(item) - - return SuccessResponse(id=item.id, msg="Item added") - - -@router.get("/items/{item_id}", response_model=DataPublicDatasetRead) -async def item_get( - item_id: int, - session: Session = Depends(get_session), -) -> DataPublicDatasetRead: - statement = select(DataPublicDataset).where(DataPublicDataset.id == item_id) - data = session.exec(statement).first() - return data - - -@router.put("/items/{item_id}", response_model=DataPublicDatasetRead) -async def item_update( - item_id: int, - data: DataPublicDatasetUpdate, - session: Session = Depends(get_session), -) -> DataPublicDatasetRead: - statement = select(DataPublicDataset).where(DataPublicDataset.id == item_id) - data_db = session.exec(statement).first() - - for field, value in data.model_dump(exclude_unset=True).items(): - setattr(data_db, field, value) - - session.add(data_db) - session.commit() - session.refresh(data_db) - - return data_db - - -@router.delete("/items/{item_id}", response_model=SuccessResponse) -async def item_delete( - item_id: int, - session: Session = Depends(get_session), -): - statement = select(DataPublicDataset).where(DataPublicDataset.id == item_id) - data = session.exec(statement).first() - session.delete(data) - session.commit() - - return SuccessResponse(id=item_id, msg="Item deleted") diff --git a/aymurai/api/endpoints/routers/database/dump.py b/aymurai/api/endpoints/routers/database/dump.py new file mode 100644 index 0000000..cbeb003 --- /dev/null +++ b/aymurai/api/endpoints/routers/database/dump.py @@ -0,0 +1,34 @@ +import os + +from fastapi import HTTPException +from fastapi.responses import FileResponse +from fastapi.routing import APIRouter + +from aymurai.logger import get_logger +from aymurai.settings import settings + +logger = get_logger(__name__) + +router = APIRouter() + + +@router.get("/database/export") +async def database_dump() -> FileResponse: + # Only support SQLite dumps for now + db_uri = str(settings.SQLALCHEMY_DATABASE_URI) + if not db_uri.startswith("sqlite:///"): + raise HTTPException( + status_code=400, + detail="Database export only supported for SQLite", + ) + + db_file = db_uri.removeprefix("sqlite:///") + if not os.path.exists(db_file): + raise HTTPException(status_code=404, detail="Database file not found") + + # Return the raw SQLite file + return FileResponse( + path=db_file, + filename="aymurai-backend.db", + media_type="application/octet-stream", + ) diff --git a/aymurai/api/endpoints/routers/datapublic/__init__.py b/aymurai/api/endpoints/routers/datapublic/__init__.py index e69de29..a164ab3 100644 --- a/aymurai/api/endpoints/routers/datapublic/__init__.py +++ b/aymurai/api/endpoints/routers/datapublic/__init__.py @@ -0,0 +1,2 @@ +# ruff: noqa: F401 +from . import dataset, validation diff --git a/aymurai/api/endpoints/routers/datapublic/datapublic.py b/aymurai/api/endpoints/routers/datapublic/datapublic.py deleted file mode 100644 index b83ae0d..0000000 --- a/aymurai/api/endpoints/routers/datapublic/datapublic.py +++ /dev/null @@ -1,161 +0,0 @@ -import os -from threading import Lock - -import torch -from fastapi import Body, Depends, Query, HTTPException -from fastapi.routing import APIRouter -from pydantic import UUID5 -from sqlmodel import Session - -from aymurai.api.utils import load_pipeline -from aymurai.database.schema import ( - DataPublicParagraph, - DataPublicDocument, - DataPublicDocumentParagraph, -) -from aymurai.database.session import get_session -from aymurai.database.utils import text_to_uuid -from aymurai.logger import get_logger -from aymurai.meta.api_interfaces import ( - DataPublicDocumentAnnotations, - DocumentInformation, - TextRequest, -) -from aymurai.settings import settings -from aymurai.utils.misc import get_element - -logger = get_logger(__name__) - - -RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads = 100 # FIXME: polemic ? -pipeline_lock = Lock() - - -router = APIRouter() - - -# MARK: Predict -@router.post("/predict/{document_id}", response_model=DocumentInformation) -async def predict_over_text( - document_id: UUID5, - text_request: TextRequest = Body({"text": "Buenos Aires, 17 de noviembre 2024"}), - use_cache: bool = Query( - True, description="Use cache to store or retrive predictions" - ), - session: Session = Depends(get_session), -) -> DocumentInformation: - logger.info("datapublic predict single") - - logger.info(f"Checking cache (use cache: {use_cache})") - text = text_request.text - paragraph_id = text_to_uuid(text) - - cached_prediction = session.get(DataPublicParagraph, paragraph_id) - if cached_prediction and use_cache: - logger.info(f"cache loaded from key: {paragraph_id}") - logger.debug(f"{cached_prediction}") - labels = cached_prediction.prediction - return DocumentInformation(document=cached_prediction.text, labels=labels or []) - - # load datapublic pipeline - logger.info("Running prediction") - item = [{"path": "empty", "data": {"doc.text": text_request.text}}] - pipeline = load_pipeline( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "full-paragraph") - ) - - with pipeline_lock: - processed = pipeline.preprocess(item) - processed = pipeline.predict_single(processed[0]) - processed = pipeline.postprocess([processed]) - - text = get_element(processed[0], ["data", "doc.text"]) or "" - labels = get_element(processed[0], ["predictions", "entities"]) or [] - - if use_cache: - logger.info(f"saving in cache: {paragraph_id}") - - document = session.get(DataPublicDocument, document_id) - if not document: - document = DataPublicDocument(id=document_id) - - paragraph = DataPublicParagraph(id=paragraph_id, text=text, prediction=labels) - document_paragraphs = DataPublicDocumentParagraph( - paragraph_id=paragraph_id, - document_id=document_id, - ) - - session.add_all([document, paragraph, document_paragraphs]) - session.commit() - session.refresh(paragraph) - - # paragraph = datapublic_paragraph_create(paragraph, session=session) - - return DocumentInformation(document=text, labels=paragraph.prediction) - - -# MARK: Validate Paragraph -# @router.post("/validation", response_model=list[DocLabel] | None) -# async def datapublic_get_paragraph_validation( -# text_request: TextRequest = Body( -# {"text": "Acusado: Ramiro Marrón DNI 34.555.666."} -# ), -# session: Session = Depends(get_session), -# ) -> list[DocLabel] | None: -# """ -# Get the validation labels for a given paragraph text. -# """ - -# text = text_request.text -# paragraph_id = text_to_uuid(text) - -# paragraph = datapublic_paragraph_read(paragraph_id, session=session) -# if not paragraph: -# return None - -# return paragraph.validation - - -# MARK: GET Validation Document -@router.get("/validation/document/{document_id}") -async def datapublic_read_document_validation( - document_id: UUID5, - session: Session = Depends(get_session), -) -> DataPublicDocumentAnnotations | None: - logger.info(f"loading annotations => {document_id}") - - document = session.get(DataPublicDocument, document_id) - if not document: - raise HTTPException( - status_code=404, - detail=f"Document not found: {document_id}", - ) - - annotations = document.validation - if not annotations: - return None - - return DataPublicDocumentAnnotations(root=annotations) - - -# MARK: POST Validation Document -@router.post("/validation/document/{document_id}") -async def datapublic_save_document_validation( - document_id: UUID5, - annotations: DataPublicDocumentAnnotations = Body(..., example={}), - session: Session = Depends(get_session), -) -> None: - logger.info(f"processing annotations => {document_id}") - - document = session.get(DataPublicDocument, document_id) - if not document: - document = DataPublicDocument(id=document_id) - - document.validation = annotations.root - session.add(document) - session.commit() - session.refresh(document) - logger.info(f"document validation saved => {document_id}") - - return diff --git a/aymurai/api/endpoints/routers/datapublic/dataset.py b/aymurai/api/endpoints/routers/datapublic/dataset.py index 8916b54..db42ffe 100644 --- a/aymurai/api/endpoints/routers/datapublic/dataset.py +++ b/aymurai/api/endpoints/routers/datapublic/dataset.py @@ -1,109 +1,123 @@ -import os import json +import os import tempfile +from datetime import datetime +from enum import Enum import pandas as pd -from pydantic import UUID4, UUID5 -from sqlmodel import Session, select -from fastapi.routing import APIRouter +from fastapi import Depends from fastapi.responses import FileResponse +from fastapi.routing import APIRouter +from sqlmodel import Session, select from starlette.background import BackgroundTask -from fastapi import Depends, UploadFile, HTTPException -from aymurai.logger import get_logger -from aymurai.database.session import get_session -from aymurai.meta.api_interfaces import SuccessResponse from aymurai.api.endpoints.meta.database import ExportFormat from aymurai.database.schema import ( - DataPublicDataset, - DataPublicDatasetRead, - DataPublicDatasetCreate, + Datapublic, + DatapublicFormat, + ModelType, ) +from aymurai.database.session import get_session +from aymurai.logger import get_logger logger = get_logger(__name__) -router = APIRouter() - - -@router.post("", response_model=DataPublicDatasetRead) -async def datapublic_dataset_create( - data: DataPublicDatasetCreate, - session: Session = Depends(get_session), -) -> DataPublicDatasetRead: - data = DataPublicDataset(**data.model_dump()) +# add a Dataset enum to include "all" and map NONE to "unsorted" +class Dataset(str, Enum): + ALL = "all" + UNSORTED = "unsorted" - with session.begin(): - session.add(data) - session.refresh(data) +router = APIRouter() - return DataPublicDatasetRead(**data.model_dump()) +def get_records( + session: Session, + dataset: Dataset = Dataset.ALL, + start_date: datetime | None = None, + end_date: datetime | None = None, + offset: int = 0, + limit: int = 100, +) -> pd.DataFrame: + # build base query + statement = select(Datapublic) + + # filter by dataset/format + if dataset != Dataset.ALL: + if dataset == Dataset.UNSORTED: + fmt = DatapublicFormat.NONE + else: + fmt = DatapublicFormat(dataset.value) + statement = statement.where(Datapublic.validation_format == fmt) -@router.get("/{document_id}", response_model=DataPublicDatasetRead) -async def datapublic_dataset_read( - document_id: UUID4 | UUID5, - session: Session = Depends(get_session), -) -> DataPublicDatasetRead: - statement = select(DataPublicDataset).where(DataPublicDataset.id == document_id) - data = session.exec(statement).first() + # optional date filters + if start_date: + statement = statement.where(Datapublic.created_at >= start_date) + if end_date: + statement = statement.where(Datapublic.created_at <= end_date) - if not data: - raise HTTPException(status_code=404, detail="Document not found") + # pagination + statement = statement.offset(offset).limit(limit) + results = session.exec(statement).all() - return DataPublicDatasetRead(**data.model_dump()) + return pd.DataFrame([item.records for item in results]) -@router.post("/batch/import/json", response_model=SuccessResponse) -async def datapublic_dataset_batch_create( - json_file: UploadFile, +@router.get(f"/pipeline/{ModelType.DATAPUBLIC}/records") +async def get_datapublic_records( + dataset: Dataset = Dataset.ALL, + start_date: datetime | None = None, + end_date: datetime | None = None, + offset: int = 0, + limit: int = 100, session: Session = Depends(get_session), -) -> SuccessResponse: - try: - data = json.load(json_file.file) - - with session.begin(): - for item in data: - dataset = DataPublicDataset.model_validate(item) - session.add(dataset) +) -> list[dict]: + df = get_records( + session=session, + dataset=dataset, + start_date=start_date, + end_date=end_date, + offset=offset, + limit=limit, + ) - except json.JSONDecodeError as e: - raise HTTPException(status_code=400, detail=f"Invalid JSON format: {str(e)}") - except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + encoded = df.to_json(orient="records", date_format="iso", index=False) - return SuccessResponse(message="Data imported successfully") + return json.loads(encoded) -@router.get("/batch/export") -async def datapublic_dataset_batch_read( - format: ExportFormat = ExportFormat.CSV, +@router.get(f"/pipeline/{ModelType.DATAPUBLIC}/records/export") +async def database_export( + dataset: Dataset = Dataset.ALL, + start_date: datetime | None = None, + end_date: datetime | None = None, + offset: int = 0, + limit: int = 100, session: Session = Depends(get_session), ) -> FileResponse: - statement = select(DataPublicDataset) - dump = session.exec(statement).all() - - # re-encode data - data = [DataPublicDatasetRead(**row.model_dump()) for row in dump] - data = [json.loads(row.model_dump_json()) for row in data] + data = get_records( + session=session, + dataset=dataset, + start_date=start_date, + end_date=end_date, + offset=offset, + limit=limit, + ) with tempfile.NamedTemporaryFile(delete=False) as temp: if format == ExportFormat.CSV: - df = pd.DataFrame(data) - df.to_csv(temp.name, index=False) + data.to_csv(temp.name, index=False) extension = "csv" elif format == ExportFormat.JSON: - temp.write(json.dumps(data).encode("utf-8")) + data.to_json(temp.name, orient="records") extension = "json" else: - raise HTTPException(status_code=400, detail="Invalid export format") - - export_filename = temp.name + raise ValueError("Invalid export format") return FileResponse( - path=export_filename, + path=temp.name, filename=f"datapublic.{extension}", + background=BackgroundTask(os.remove, temp.name), media_type="application/octet-stream", - background=BackgroundTask(os.remove, export_filename), ) diff --git a/aymurai/api/endpoints/routers/datapublic/validation.py b/aymurai/api/endpoints/routers/datapublic/validation.py new file mode 100644 index 0000000..5690c56 --- /dev/null +++ b/aymurai/api/endpoints/routers/datapublic/validation.py @@ -0,0 +1,80 @@ +from fastapi import Body, Depends, HTTPException +from fastapi.responses import JSONResponse +from fastapi.routing import APIRouter +from pydantic import UUID4, UUID5 +from sqlmodel import Session, select +from starlette.status import HTTP_201_CREATED + +from aymurai.database.schema import ( + Datapublic, + DatapublicFormat, + DatapublicPublic, + ModelType, +) +from aymurai.database.session import get_session +from aymurai.logger import get_logger + +logger = get_logger(__name__) + + +router = APIRouter() + + +# MARK: GET Validation Document +@router.get(f"/pipeline/{ModelType.DATAPUBLIC}/document/{{document_id}}/validation") +async def datapublic_get_document_validation( + document_id: UUID4 | UUID5, + session: Session = Depends(get_session), +) -> DatapublicPublic: + statement = select(Datapublic).where(Datapublic.fk_document == document_id) + datapublic = session.exec(statement).first() + if not datapublic: + raise HTTPException( + status_code=404, + detail=f"datapublic validation not found for document: {document_id}", + ) + + logger.info(f"datapublic validation loaded for document: {document_id}") + + return DatapublicPublic.model_validation(datapublic) + + +# MARK: POST Validation Document +@router.post(f"/pipeline/{ModelType.DATAPUBLIC}/document/{{document_id}}/validation") +async def datapublic_save_document_validation( + document_id: UUID4 | UUID5, + records: dict = Body(..., example={}), + validation_format: DatapublicFormat = DatapublicFormat.NONE, + session: Session = Depends(get_session), +) -> JSONResponse: + logger.info(f"processing annotations => {document_id}") + + statement = select(Datapublic).where(Datapublic.fk_document == document_id) + datapublic = session.exec(statement).first() + if not datapublic: + datapublic = Datapublic( + fk_document=document_id, + records={}, + validation_format=validation_format, + ) + + match validation_format: + case DatapublicFormat.NONE: + logger.info(f"validation format: {validation_format}. skipping") + case _: + NotImplementedError( + f"validation format: {validation_format} not implemented" + ) + + datapublic.records = records + datapublic.validation_format = validation_format + + session.add(datapublic) + session.commit() + + logger.info(f"datapublic validation saved for document: {document_id}") + + return JSONResponse( + status_code=HTTP_201_CREATED, + content={"message": f"datapublic validation saved for document: {document_id}"}, + ) diff --git a/aymurai/api/endpoints/routers/document/__init__.py b/aymurai/api/endpoints/routers/document/__init__.py index 979d6fc..32d24e2 100644 --- a/aymurai/api/endpoints/routers/document/__init__.py +++ b/aymurai/api/endpoints/routers/document/__init__.py @@ -1,2 +1,2 @@ # ruff: noqa: F401 -from . import anonymizer, convert, extract +from . import convert, extract, predict diff --git a/aymurai/api/endpoints/routers/document/convert.py b/aymurai/api/endpoints/routers/document/convert.py index 8a67b7e..ac274fa 100644 --- a/aymurai/api/endpoints/routers/document/convert.py +++ b/aymurai/api/endpoints/routers/document/convert.py @@ -6,12 +6,12 @@ import pymupdf4llm import pypandoc -from fastapi import Path, Query, UploadFile +from fastapi import HTTPException, Path, Query, UploadFile from fastapi.responses import FileResponse from fastapi.routing import APIRouter +from starlette import status from starlette.background import BackgroundTask -from aymurai.api.exceptions import UnsupportedFileType from aymurai.logger import get_logger from aymurai.settings import settings @@ -76,7 +76,10 @@ async def convert_pdf_pandoc( ) -> FileResponse: _, suffix = os.path.splitext(file.filename) if suffix != ".pdf": - raise UnsupportedFileType(detail="Expected a .pdf file") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Expected a .pdf file", + ) with tempfile.NamedTemporaryFile(suffix=".pdf", delete=True) as tmp_file: tmp_file.write(file.file.read()) @@ -109,9 +112,15 @@ async def convert_file( ) -> FileResponse: _, suffix = os.path.splitext(file.filename) if suffix.lower() != f".{source.value}": - raise UnsupportedFileType(detail=f"Expected a .{source.value} file") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUES, + detail=f"Expected a .{source.value} file", + ) if source == target: - raise UnsupportedFileType(detail="Source and target formats are the same.") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Source and target formats are the same.", + ) # Set extra_args for libreoffice conversions extra_args = '--infilter="writer_pdf_import"' if source == FileFormat.pdf else "" @@ -126,9 +135,10 @@ async def convert_file( extra_args=extra_args, ) case _: - raise UnsupportedFileType( + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=( f"Conversion from {source.value} to {target.value} " f"with backend {backend} is not supported." - ) + ), ) diff --git a/aymurai/api/endpoints/routers/document/extract.py b/aymurai/api/endpoints/routers/document/extract.py index 51d1e8e..98477e9 100644 --- a/aymurai/api/endpoints/routers/document/extract.py +++ b/aymurai/api/endpoints/routers/document/extract.py @@ -1,7 +1,7 @@ import os import re import tempfile -from threading import Lock +import threading from fastapi import Depends, UploadFile from fastapi.routing import APIRouter @@ -18,8 +18,8 @@ from aymurai.utils.misc import get_element logger = get_logger(__name__) -pipeline_lock = Lock() +lock = threading.Lock() router = APIRouter() @@ -58,7 +58,7 @@ def plain_text_extractor( logger.info("processing data item") logger.info(f"{item}") - with pipeline_lock: + with lock: processed = pipeline.preprocess([item]) except Exception as e: diff --git a/aymurai/api/endpoints/routers/document/predict.py b/aymurai/api/endpoints/routers/document/predict.py new file mode 100644 index 0000000..1bec869 --- /dev/null +++ b/aymurai/api/endpoints/routers/document/predict.py @@ -0,0 +1,50 @@ +from fastapi import Depends, HTTPException +from fastapi.routing import APIRouter +from more_itertools import flatten +from pydantic import UUID4, UUID5 +from sqlmodel import Session + +from aymurai.database.crud.prediction import ( + read_document_prediction_paragraphs, +) +from aymurai.database.meta.document import Document +from aymurai.database.schema import ( + ModelType, +) +from aymurai.database.session import get_session +from aymurai.logger import get_logger +from aymurai.meta.entities import DocLabel +from aymurai.settings import settings + +logger = get_logger(__name__) + + +RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH + + +router = APIRouter() + + +# MARK: Get Document labels +@router.get("/pipeline/{pipeline_type}/document/{document_id}") +async def get_document_labels( + document_id: UUID4 | UUID5, + pipeline_type: ModelType, + session: Session = Depends(get_session), +) -> list[DocLabel]: + # ———————— Sanity check —————————————————————————————————————————————————————————— + document = session.get(Document, document_id) + if not document: + raise HTTPException( + status_code=404, + detail=f"Document not found: {document_id}", + ) + + annotations = read_document_prediction_paragraphs( + session=session, + document_id=document_id, + model_type=pipeline_type, + ) + + labels = [para.prediction.labels for para in annotations if para.prediction] + return list(flatten(labels)) diff --git a/aymurai/api/endpoints/routers/document/search.py b/aymurai/api/endpoints/routers/document/search.py new file mode 100644 index 0000000..dd1ef37 --- /dev/null +++ b/aymurai/api/endpoints/routers/document/search.py @@ -0,0 +1,154 @@ +import unicodedata +import uuid + +import numpy as np +import pandas as pd +import regex +from fastapi import Depends, HTTPException +from fastapi.routing import APIRouter +from more_itertools import flatten +from pydantic import UUID4, UUID5 +from sklearn.cluster import DBSCAN +from sklearn.feature_extraction.text import CountVectorizer +from sqlmodel import Session + +from aymurai.database.crud.prediction import read_document_prediction_paragraphs +from aymurai.database.schema import Document, ModelType +from aymurai.database.session import get_session +from aymurai.logger import get_logger +from aymurai.meta.entities import DocLabel + +logger = get_logger(__name__) + +router = APIRouter() + + +# Normalize helper +def normalize_text(text): + text = unicodedata.normalize("NFKD", text) + text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn") + text = regex.sub(r"\s+", " ", text) + text = regex.sub(r"\p{P}", "", text) + return text.lower() + + +def cluster_entities(entities: list[DocLabel], eps: float = 0.01, min_samples: int = 2): + """ + Cluster entity texts in a group and return a DataFrame with columns: + text, norm_text, index, cluster. Uses binary vectorization and Jaccard-DBSCAN. + """ + + indexed_entities = [(i, entity) for i, entity in enumerate(entities)] + indexed_entities = sorted(indexed_entities, key=lambda x: x[1].attrs.aymurai_label) + + # Prepare raw and normalized texts + texts = [ent[1].text for ent in indexed_entities] + norm_texts = [normalize_text(t) for t in texts] + + # --------- Binary vectorization of words ---------------- + vectorizer = CountVectorizer(binary=True, token_pattern=r"\b\w+\b") + matrix = vectorizer.fit_transform( + norm_texts + ) # sparse matrix (n_samples x n_features) + + # --------- Jaccard distance matrix ---------------- + ints = (matrix @ matrix.T).toarray() + row_sums = matrix.sum(axis=1).A1 + union = row_sums[:, None] + row_sums[None, :] - ints + # Avoid division by zero and compute distances + with np.errstate(divide="ignore", invalid="ignore"): + X = 1 - (ints / union) + X[union == 0] = 1.0 + + # -------- DBSCAN ---------------------------------------------------- + db = DBSCAN(eps=eps, min_samples=min_samples, metric="precomputed") + labels = db.fit_predict(X) + + # Merge clusters by overlapping centroids + unique_lbls = sorted(set(labels) - {-1}) + if unique_lbls: + # build centroid binary vectors + centroid_vectors = [] + for lbl in unique_lbls: + mat = matrix[labels == lbl] + centroid = (mat.sum(axis=0) > 0).A1 # boolean mask + centroid_vectors.append(centroid) + + C = np.vstack(centroid_vectors).astype(bool) + sim = (C.astype(int) @ C.T.astype(int)) > 0 + mapping = { + lbl: unique_lbls[int(np.argmax(sim[idx]))] + for idx, lbl in enumerate(unique_lbls) + } + labels = [mapping.get(lbl, -1) for lbl in labels] + + # Build results DataFrame + df = ( + pd.DataFrame( + { + "id": [ent[1].id for ent in indexed_entities], + "index": [ent[0] for ent in indexed_entities], + "paragraph_id": [ent[1].fk_paragraph for ent in indexed_entities], + "label": [ent[1].attrs.aymurai_label for ent in indexed_entities], + "text": texts, + "norm_text": norm_texts, + "cluster": labels, + } + ) + .sort_values("cluster") + .reset_index(drop=True) + ) + df["id"] = df["id"].astype(uuid.UUID) + df["paragraph_id"] = df["paragraph_id"].astype(uuid.UUID) + df.set_index("id", inplace=True) + df.sort_values(by=["cluster", "index"], inplace=True) + + return df + + +@router.get("/pipeline/{pipeline_type}/document/{document_id}/search") +async def search( + pipeline_type: ModelType, + document_id: UUID4 | UUID5, + query: str | None = None, + label_id: UUID4 | UUID5 | None = None, + session: Session = Depends(get_session), +) -> list[DocLabel] | None: + # ———————— Sanity check ———————————————————————————————————————————————————————— + if (not query and not label_id) or (query and label_id): + raise HTTPException( + status_code=400, + detail="Either query or label_id must be provided", + ) + + document = session.get(Document, document_id) + if not document: + raise HTTPException(status_code=404, detail="Document not found") + + # ———————— Get annotations ———————————————————————————————————————————————————————— + annotations = read_document_prediction_paragraphs( + session=session, + document_id=document_id, + model_type=pipeline_type, + ) + + labels = [para.prediction.labels for para in annotations if para.prediction] + entities = list(flatten(labels)) + + clusters_df = cluster_entities(entities) + + if label_id: + label_cluster = clusters_df.loc[label_id, "cluster"] + cluster = clusters_df[clusters_df["cluster"] == label_cluster] + + elif query: + query = normalize_text(query) + cluster = cluster[cluster["norm_text"].str.contains(query, na=False)] + + if cluster.empty: + return None + + indices = cluster.index.tolist() + entities = [entities[i] for i in indices] + + return entities diff --git a/aymurai/api/endpoints/routers/paragraph/predict.py b/aymurai/api/endpoints/routers/paragraph/predict.py index 14da0d2..ea597c0 100644 --- a/aymurai/api/endpoints/routers/paragraph/predict.py +++ b/aymurai/api/endpoints/routers/paragraph/predict.py @@ -1,6 +1,5 @@ -from threading import Lock +import threading -import torch from fastapi import Depends, HTTPException, Query from fastapi.routing import APIRouter from pydantic import UUID4 @@ -27,16 +26,15 @@ RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads(100) # FIXME: polemic ? -pipeline_lock = Lock() +prediction_lock = threading.Lock() router = APIRouter() # MARK: Paragraph Predict @router.get( - "/paragraph/{paragraph_id}/pipeline/{pipeline_type}/predict", + "/pipeline/{pipeline_type}/paragraph/{paragraph_id}/predict", response_model=ParagraphPredictionPublic, ) async def paragraph_predict( @@ -101,7 +99,7 @@ async def paragraph_predict( logger.info("Running prediction") item = [{"path": "empty", "data": {"doc.text": input_text}}] - with pipeline_lock: + with prediction_lock: processed = pipeline.preprocess(item) processed = pipeline.predict_single(processed[0]) processed = pipeline.postprocess([processed]) @@ -157,7 +155,7 @@ async def paragraph_predict( # MARK: Paragraph Validate -@router.patch("/paragraph/{paragraph_id}/pipeline/{pipeline_type}/validate") +@router.patch("/pipeline/{pipeline_type}/paragraph/{paragraph_id}/validate") async def save_paragraph_validation( paragraph_id: UUID4, pipeline_type: ModelType, diff --git a/aymurai/api/exceptions.py b/aymurai/api/exceptions.py deleted file mode 100644 index de6e5ed..0000000 --- a/aymurai/api/exceptions.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import HTTPException - - -class AymuraiAPIException(HTTPException): - status_code: int - title: str - - def __init__(self, status_code: int = None, detail: str = None): - self.status_code = status_code or self.status_code - self.detail = f"{self.title}: {detail}" if detail else self.title - super().__init__(status_code=self.status_code, detail=detail) - - -class UnsupportedFileType(AymuraiAPIException): - status_code = 400 - title = "Unsupported file type" diff --git a/aymurai/api/main.py b/aymurai/api/main.py index 29c63f0..e113070 100644 --- a/aymurai/api/main.py +++ b/aymurai/api/main.py @@ -15,12 +15,12 @@ from aymurai.database.session import get_session from aymurai.logger import get_logger from aymurai.pipeline import AymurAIPipeline -from aymurai.settings import settings, Settings +from aymurai.settings import Settings, settings logger = get_logger(__name__) -torch.set_num_threads = 100 # FIXME: polemic ? +torch.set_num_threads(100) # FIXME: polemic ? RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH @@ -34,10 +34,10 @@ async def lifespan(app: FastAPI): try: check_db_connection() logger.info(">> Running Alembic migrations") - alembic_cfg = Config(str(settings.ALEMBIC_INI_PATH)) + alembic_cfg = Config(str(settings.ALEMBIC_CONFIG)) command.upgrade(alembic_cfg, "head") except Exception as error: - logger.error("Error while starting up:", error) + logger.exception(f"Error while starting up: {error}") # ------ Register pipelines ------------------------------------------------------ logger.info(">> Registering models") @@ -72,7 +72,6 @@ async def lifespan(app: FastAPI): title="AymurAI API", version=settings.APP_VERSION, lifespan=lifespan, - docs_url="/docs" if not settings.SWAGGER_UI_DARK_MODE else None, ) @@ -101,21 +100,7 @@ async def add_process_time_header(request: Request, call_next): @api.get("/", response_class=RedirectResponse, include_in_schema=False) async def index(): - return "/docs" - - -if settings.SWAGGER_UI_DARK_MODE: - from fastapi.openapi.docs import get_swagger_ui_html - - api.docs_url = None - - @api.get("/docs", include_in_schema=False) - async def custom_swagger_ui_html(): - return get_swagger_ui_html( - openapi_url=api.openapi_url, - title=f"{api.title} - Swagger UI", - swagger_css_url="https://cdn.jsdelivr.net/gh/danielperezrubio/swagger-dark-theme@main/assets/swagger-ui.min.css", - ) + return RedirectResponse(url="/docs") ################################################################################ diff --git a/aymurai/api/startup/database.py b/aymurai/api/startup/database.py index 50ec7e3..78b3c63 100644 --- a/aymurai/api/startup/database.py +++ b/aymurai/api/startup/database.py @@ -1,11 +1,11 @@ -import os import logging +import os from sqlmodel import select -from tenacity import retry, after_log, before_log, wait_fixed, stop_after_attempt +from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed -from aymurai.settings import settings from aymurai.database.session import get_session +from aymurai.settings import settings logger = logging.getLogger(__name__) diff --git a/aymurai/database/meta/anonymization/__init__.py b/aymurai/database/meta/anonymization/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/aymurai/database/meta/anonymization/document.py b/aymurai/database/meta/anonymization/document.py deleted file mode 100644 index d8d4b2d..0000000 --- a/aymurai/database/meta/anonymization/document.py +++ /dev/null @@ -1,45 +0,0 @@ -import uuid -from datetime import datetime -from typing import TYPE_CHECKING - -from pydantic import BaseModel -from sqlmodel import Field, SQLModel, Relationship -from sqlalchemy import Column, DateTime, func, text - -from aymurai.database.meta.anonymization.document_paragraph import ( - AnonymizationDocumentParagraph, -) - -if TYPE_CHECKING: - from aymurai.database.meta.anonymization.paragraph import AnonymizationParagraph - - -class AnonymizationDocument(SQLModel, table=True): - __tablename__ = "anonymization_document" - id: uuid.UUID | None = Field(None, primary_key=True) - created_at: datetime = Field( - sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} - ) - updated_at: datetime | None = Field( - sa_column=Column(DateTime(), onupdate=func.now()) - ) - - name: str = Field(nullable=False) - paragraphs: list["AnonymizationParagraph"] = Relationship( - back_populates="documents", link_model=AnonymizationDocumentParagraph - ) - - -class AnonymizationDocumentCreate(BaseModel): - name: str - # paragraphs: list["AnonymizationParagraph"] - - -class AnonymizationDocumentUpdate(BaseModel): - name: str | None = None - - -class AnonymizationDocumentRead(BaseModel): - id: uuid.UUID - name: str - # paragraphs: list[uuid.UUID] | None diff --git a/aymurai/database/meta/anonymization/document_paragraph.py b/aymurai/database/meta/anonymization/document_paragraph.py deleted file mode 100644 index c25eca1..0000000 --- a/aymurai/database/meta/anonymization/document_paragraph.py +++ /dev/null @@ -1,21 +0,0 @@ -import uuid - -from sqlmodel import Field, SQLModel - - -class AnonymizationDocumentParagraph(SQLModel, table=True): - __tablename__ = "anonymization_document_paragraph" - # NOTE: The id is required to allow repition of document_id and paragraph_id - # (the paragraph is repeated multiple times in the document) - id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) - document_id: uuid.UUID | None = Field( - None, - foreign_key="anonymization_document.id", - primary_key=True, - ) - paragraph_id: uuid.UUID | None = Field( - None, - foreign_key="anonymization_paragraph.id", - primary_key=True, - ) - order: int | None = Field(nullable=True) diff --git a/aymurai/database/meta/anonymization/paragraph.py b/aymurai/database/meta/anonymization/paragraph.py deleted file mode 100644 index 9a6e29b..0000000 --- a/aymurai/database/meta/anonymization/paragraph.py +++ /dev/null @@ -1,67 +0,0 @@ -import uuid -from datetime import datetime - -from pydantic import BaseModel, model_validator -from typing_extensions import TYPE_CHECKING, Self -from sqlmodel import Field, SQLModel, Relationship -from sqlalchemy import JSON, Column, DateTime, func, text - -from aymurai.database.utils import text_to_uuid -from aymurai.meta.api_interfaces import DocLabel - -if TYPE_CHECKING: - from aymurai.database.meta.anonymization.document import AnonymizationDocument - -from aymurai.database.meta.anonymization.document_paragraph import ( - AnonymizationDocumentParagraph, -) - - -class AnonymizationParagraphBase(SQLModel): - id: uuid.UUID | None = Field(None, primary_key=True) - - text: str = Field(nullable=False) - prediction: list[DocLabel] | None = Field(None, sa_column=Column(JSON)) - validation: list[DocLabel] | None = Field(None, sa_column=Column(JSON)) - - @model_validator(mode="after") - def validate_text(self) -> Self: - self.id = text_to_uuid(self.text) - return self - - -class AnonymizationParagraph(AnonymizationParagraphBase, table=True): - __tablename__ = "anonymization_paragraph" - - # NOTE: SQLModel with table=True does not run validators - id: uuid.UUID | None = Field(None, primary_key=True) - created_at: datetime = Field( - sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} - ) - updated_at: datetime | None = Field( - sa_column=Column(DateTime(), onupdate=func.now()) - ) - documents: list["AnonymizationDocument"] = Relationship( - back_populates="paragraphs", - link_model=AnonymizationDocumentParagraph, - ) - - -class AnonymizationParagraphCreate(AnonymizationParagraphBase): - pass - - -class AnonymizationParagraphUpdate(BaseModel): - prediction: list[DocLabel] | None = None - validation: list[DocLabel] | None = None - - -class AnonymizationParagraphRead(BaseModel): - id: uuid.UUID - - text: str - prediction: list[DocLabel] | None - validation: list[DocLabel] | None - - created_at: datetime - updated_at: datetime | None diff --git a/aymurai/database/meta/datapublic.py b/aymurai/database/meta/datapublic.py new file mode 100644 index 0000000..4b01d60 --- /dev/null +++ b/aymurai/database/meta/datapublic.py @@ -0,0 +1,44 @@ +import enum +import uuid +from datetime import datetime + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import JSON, Column, DateTime, func, text +from sqlmodel import Field, Relationship, SQLModel + +from aymurai.database.meta.document import Document + + +class DatapublicFormat(str, enum.Enum): + NONE = "none" + + +class Datapublic(SQLModel, table=True): + __tablename__ = "datapublic" + + id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) + + created_at: datetime = Field( + sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} + ) + updated_at: datetime | None = Field( + sa_column=Column(DateTime(), onupdate=func.now()) + ) + + validation_format: DatapublicFormat = DatapublicFormat.NONE + + records: dict = Field(sa_column=Column(JSON)) + + fk_document: uuid.UUID | None = Field(foreign_key="document.id") + document: Document | None = Relationship(back_populates="datapublic") + + +class DatapublicPublic(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: uuid.UUID + fk_document: uuid.UUID + created_at: datetime + updated_at: datetime | None = None + validation_format: DatapublicFormat = DatapublicFormat.NONE + records: dict = {} diff --git a/aymurai/database/meta/datapublic/__init__.py b/aymurai/database/meta/datapublic/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/aymurai/database/meta/datapublic/document.py b/aymurai/database/meta/datapublic/document.py deleted file mode 100644 index 5f0ac16..0000000 --- a/aymurai/database/meta/datapublic/document.py +++ /dev/null @@ -1,54 +0,0 @@ -import uuid -from datetime import datetime - -from typing import TYPE_CHECKING -from sqlmodel import Field, SQLModel, Relationship -from sqlalchemy import Column, DateTime, func, text, JSON -from aymurai.meta.api_interfaces import DataPublicDocumentAnnotations - -from aymurai.database.meta.datapublic.document_paragraph import ( - DataPublicDocumentParagraph, -) - -if TYPE_CHECKING: - from aymurai.database.meta.datapublic.paragraph import DataPublicParagraph - - -class DataPublicDocumentBase(SQLModel): - prediction: DataPublicDocumentAnnotations | None = Field( - None, sa_column=Column(JSON) - ) - validation: DataPublicDocumentAnnotations | None = Field( - None, sa_column=Column(JSON) - ) - - -class DataPublicDocument(DataPublicDocumentBase, table=True): - __tablename__ = "datapublic_document" - id: uuid.UUID | None = Field(None, primary_key=True) - created_at: datetime = Field( - sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} - ) - updated_at: datetime | None = Field( - sa_column=Column(DateTime(), onupdate=func.now()) - ) - - paragraphs: list["DataPublicParagraph"] = Relationship( - back_populates="documents", - link_model=DataPublicDocumentParagraph, - ) - - -class DataPublicDocumentCreate(DataPublicDocumentBase): - pass - - -class DataPublicDocumentUpdate(DataPublicDocumentBase): - prediction: DataPublicDocumentAnnotations | None = None - validation: DataPublicDocumentAnnotations | None = None - - -class DataPublicDocumentRead(DataPublicDocumentBase): - id: uuid.UUID - created_at: datetime - updated_at: datetime | None = None diff --git a/aymurai/database/meta/datapublic/document_paragraph.py b/aymurai/database/meta/datapublic/document_paragraph.py deleted file mode 100644 index b3d51b3..0000000 --- a/aymurai/database/meta/datapublic/document_paragraph.py +++ /dev/null @@ -1,21 +0,0 @@ -import uuid - -from sqlmodel import Field, SQLModel - - -class DataPublicDocumentParagraph(SQLModel, table=True): - __tablename__ = "datapublic_document_paragraph" - # NOTE: The id is required to allow repition of document_id and paragraph_id - # (the paragraph is repeated multiple times in the document) - id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) - document_id: uuid.UUID | None = Field( - None, - foreign_key="datapublic_document.id", - primary_key=True, - ) - paragraph_id: uuid.UUID | None = Field( - None, - foreign_key="datapublic_paragraph.id", - primary_key=True, - ) - order: int | None = Field(nullable=True) diff --git a/aymurai/database/meta/datapublic/paragraph.py b/aymurai/database/meta/datapublic/paragraph.py deleted file mode 100644 index bd5edb2..0000000 --- a/aymurai/database/meta/datapublic/paragraph.py +++ /dev/null @@ -1,67 +0,0 @@ -import uuid -from datetime import datetime - -from pydantic import BaseModel, model_validator -from typing_extensions import TYPE_CHECKING, Self -from sqlmodel import Field, SQLModel, Relationship -from sqlalchemy import JSON, Column, DateTime, func, text - -from aymurai.database.utils import text_to_uuid -from aymurai.meta.api_interfaces import DocLabel - -if TYPE_CHECKING: - from aymurai.database.meta.datapublic.document import DataPublicDocument - -from aymurai.database.meta.datapublic.document_paragraph import ( - DataPublicDocumentParagraph, -) - - -class DataPublicParagraphBase(SQLModel): - id: uuid.UUID | None = Field(None, primary_key=True) - - text: str = Field(nullable=False) - prediction: list[DocLabel] | None = Field(None, sa_column=Column(JSON)) - validation: list[DocLabel] | None = Field(None, sa_column=Column(JSON)) - - @model_validator(mode="after") - def validate_text(self) -> Self: - self.id = text_to_uuid(self.text) - return self - - -class DataPublicParagraph(DataPublicParagraphBase, table=True): - __tablename__ = "datapublic_paragraph" - - # NOTE: SQLModel with table=True does not run validators - id: uuid.UUID | None = Field(None, primary_key=True) - created_at: datetime = Field( - sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP")} - ) - updated_at: datetime | None = Field( - sa_column=Column(DateTime(), onupdate=func.now()) - ) - documents: list["DataPublicDocument"] = Relationship( - back_populates="paragraphs", - link_model=DataPublicDocumentParagraph, - ) - - -class DataPublicParagraphCreate(DataPublicParagraphBase): - pass - - -class DataPublicParagraphUpdate(BaseModel): - prediction: list[DocLabel] | None = None - validation: list[DocLabel] | None = None - - -class DataPublicParagraphRead(BaseModel): - id: uuid.UUID - - text: str - prediction: list[DocLabel] | None - validation: list[DocLabel] | None - - created_at: datetime - updated_at: datetime | None diff --git a/aymurai/database/meta/document.py b/aymurai/database/meta/document.py index a56c409..ba6d578 100644 --- a/aymurai/database/meta/document.py +++ b/aymurai/database/meta/document.py @@ -1,11 +1,17 @@ import uuid from datetime import datetime +from typing import TYPE_CHECKING from pydantic import BaseModel -from sqlmodel import Field, SQLModel, Relationship -from sqlalchemy import Column, DateTime, func, text, LargeBinary +from sqlalchemy import Column, DateTime, LargeBinary, func, text +from sqlalchemy.orm import deferred +from sqlmodel import Field, Relationship, SQLModel -from aymurai.database.meta.paragraph import Paragraph, ParagraphPublic +from aymurai.database.meta.paragraph import ParagraphPublic + +if TYPE_CHECKING: + from aymurai.database.meta.datapublic import Datapublic + from aymurai.database.meta.paragraph import Paragraph, ParagraphPublic class Document(SQLModel, table=True): @@ -13,7 +19,7 @@ class Document(SQLModel, table=True): id: uuid.UUID | None = Field(None, primary_key=True) data: bytes = Field( - sa_column=Column(LargeBinary), + sa_column=deferred(Column(LargeBinary)), description="binary data of the document", ) created_at: datetime = Field( @@ -26,6 +32,7 @@ class Document(SQLModel, table=True): name: str = Field(nullable=False) paragraphs: list["Paragraph"] = Relationship(back_populates="document") + datapublic: list["Datapublic"] = Relationship(back_populates="document") class DocumentUpdate(BaseModel): diff --git a/aymurai/database/meta/prediction.py b/aymurai/database/meta/prediction.py index b3da7fd..6fee404 100644 --- a/aymurai/database/meta/prediction.py +++ b/aymurai/database/meta/prediction.py @@ -28,6 +28,15 @@ class PredictionBase(SQLModel): @model_validator(mode="after") def validate_input(self) -> "PredictionBase": self.input_hash = text_to_hash(self.input) + + # set the paragraph id in doc labels + if self.validation: + for label in self.validation: + label.fk_paragraph = str(self.fk_paragraph) + if self.prediction: + for label in self.prediction: + label.fk_paragraph = str(self.fk_paragraph) + return self diff --git a/aymurai/database/schema.py b/aymurai/database/schema.py index c2c7c97..68048e8 100644 --- a/aymurai/database/schema.py +++ b/aymurai/database/schema.py @@ -1,31 +1,5 @@ # ruff: noqa: F401 -from .meta.anonymization.document import ( - AnonymizationDocument, - AnonymizationDocumentCreate, - AnonymizationDocumentParagraph, - AnonymizationDocumentRead, - AnonymizationDocumentUpdate, -) -from .meta.anonymization.paragraph import ( - AnonymizationParagraph, - AnonymizationParagraphCreate, - AnonymizationParagraphRead, - AnonymizationParagraphUpdate, -) -from .meta.datapublic.document import ( - DataPublicDocument, - DataPublicDocumentBase, - DataPublicDocumentCreate, - DataPublicDocumentRead, - DataPublicDocumentUpdate, -) -from .meta.datapublic.document_paragraph import DataPublicDocumentParagraph -from .meta.datapublic.paragraph import ( - DataPublicParagraph, - DataPublicParagraphCreate, - DataPublicParagraphRead, - DataPublicParagraphUpdate, -) +from .meta.datapublic import Datapublic, DatapublicFormat, DatapublicPublic from .meta.document import Document, DocumentPublic, DocumentUpdate from .meta.model import Model, ModelCreate, ModelPublic, ModelType from .meta.paragraph import Paragraph, ParagraphPublic, ParagraphUpdate diff --git a/aymurai/database/versions/d6c141e03875_create_database.py b/aymurai/database/versions/86bdf2a39c7e_create_database.py similarity index 54% rename from aymurai/database/versions/d6c141e03875_create_database.py rename to aymurai/database/versions/86bdf2a39c7e_create_database.py index bc1d5a4..b351809 100644 --- a/aymurai/database/versions/d6c141e03875_create_database.py +++ b/aymurai/database/versions/86bdf2a39c7e_create_database.py @@ -1,8 +1,8 @@ """Create database -Revision ID: d6c141e03875 +Revision ID: 86bdf2a39c7e Revises: -Create Date: 2025-05-12 15:56:52.453998 +Create Date: 2025-05-25 03:29:14.201556 """ from typing import Sequence, Union @@ -13,7 +13,7 @@ # revision identifiers, used by Alembic. -revision: str = "d6c141e03875" +revision: str = "86bdf2a39c7e" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -22,8 +22,9 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - "anonymization_document", + "document", sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("data", sa.LargeBinary(), nullable=False), sa.Column( "created_at", sa.DateTime(), @@ -35,39 +36,26 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("id"), ) op.create_table( - "anonymization_paragraph", - sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("prediction", sa.JSON(), nullable=True), - sa.Column("validation", sa.JSON(), nullable=True), + "model", sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), + "type", + sa.Enum("ANONYMIZATION", "DATAPUBLIC", name="modeltype"), nullable=False, ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "datapublic_document", - sa.Column("prediction", sa.JSON(), nullable=True), - sa.Column("validation", sa.JSON(), nullable=True), - sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("pipeline_path", sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column( "created_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False, ), - sa.Column("updated_at", sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint("id"), ) op.create_table( - "datapublic_paragraph", - sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("prediction", sa.JSON(), nullable=True), - sa.Column("validation", sa.JSON(), nullable=True), + "datapublic", sa.Column("id", sa.Uuid(), nullable=False), sa.Column( "created_at", @@ -76,72 +64,18 @@ def upgrade() -> None: nullable=False, ), sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "document", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("data", sa.LargeBinary(), nullable=True), sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), + "validation_format", + sa.Enum("NONE", name="datapublicformat"), nullable=False, ), - sa.Column("updated_at", sa.DateTime(), nullable=True), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "model", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column( - "type", - sa.Enum("ANONYMIZATION", "DATAPUBLIC", name="modeltype"), - nullable=False, - ), - sa.Column("pipeline_path", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(), - server_default=sa.text("(CURRENT_TIMESTAMP)"), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "anonymization_document_paragraph", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("document_id", sa.Uuid(), nullable=False), - sa.Column("paragraph_id", sa.Uuid(), nullable=False), - sa.Column("order", sa.Integer(), nullable=True), + sa.Column("records", sa.JSON(), nullable=True), + sa.Column("fk_document", sa.Uuid(), nullable=True), sa.ForeignKeyConstraint( - ["document_id"], - ["anonymization_document.id"], - ), - sa.ForeignKeyConstraint( - ["paragraph_id"], - ["anonymization_paragraph.id"], - ), - sa.PrimaryKeyConstraint("id", "document_id", "paragraph_id"), - ) - op.create_table( - "datapublic_document_paragraph", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("document_id", sa.Uuid(), nullable=False), - sa.Column("paragraph_id", sa.Uuid(), nullable=False), - sa.Column("order", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ["document_id"], - ["datapublic_document.id"], - ), - sa.ForeignKeyConstraint( - ["paragraph_id"], - ["datapublic_paragraph.id"], + ["fk_document"], + ["document.id"], ), - sa.PrimaryKeyConstraint("id", "document_id", "paragraph_id"), + sa.PrimaryKeyConstraint("id"), ) op.create_table( "paragraph", @@ -195,12 +129,7 @@ def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_table("prediction") op.drop_table("paragraph") - op.drop_table("datapublic_document_paragraph") - op.drop_table("anonymization_document_paragraph") + op.drop_table("datapublic") op.drop_table("model") op.drop_table("document") - op.drop_table("datapublic_paragraph") - op.drop_table("datapublic_document") - op.drop_table("anonymization_paragraph") - op.drop_table("anonymization_document") # ### end Alembic commands ### diff --git a/aymurai/meta/entities.py b/aymurai/meta/entities.py index 951609f..28e2fd6 100644 --- a/aymurai/meta/entities.py +++ b/aymurai/meta/entities.py @@ -1,4 +1,6 @@ -from pydantic import BaseModel, Field +import uuid + +from pydantic import BaseModel, Field, model_validator class EntityAttributes(BaseModel): @@ -43,6 +45,9 @@ class Entity(BaseModel): class DocLabel(BaseModel): """Datatype for a document label""" + id: str | None = None + fk_paragraph: str | None = None + text: str = Field( description="raw text of entity", # alias=AliasChoices(["text", "document"]), @@ -54,3 +59,10 @@ class DocLabel(BaseModel): description="last character of the span in relation of the full text" ) attrs: EntityAttributes + + @model_validator(mode="after") + def _(self) -> "DocLabel": + """Validate the DocLabel attributes.""" + if not self.id: + self.id = str(uuid.uuid5(uuid.NAMESPACE_DNS, self.attrs.model_dump_json())) + return self diff --git a/aymurai/settings.py b/aymurai/settings.py index b364592..ebbe886 100644 --- a/aymurai/settings.py +++ b/aymurai/settings.py @@ -5,8 +5,9 @@ from dotenv import load_dotenv from pydantic import ConfigDict, FilePath, field_validator from pydantic_settings import BaseSettings -from aymurai.logger import get_logger + import aymurai +from aymurai.logger import get_logger try: from aymurai.version import __version__ @@ -66,7 +67,7 @@ def assemble_cors_origins(cls, v) -> list[str]: RESOURCES_BASEPATH: str = "/resources" # Alembic Config for running migrations - ALEMBIC_INI_PATH: FilePath = PARENT / "alembic.ini" + ALEMBIC_CONFIG: FilePath = PARENT / "alembic.ini" ENV: str | None = None @@ -77,7 +78,6 @@ def assemble_cors_origins(cls, v) -> list[str]: LIBREOFFICE_BIN: str = "libreoffice" # ----- Miscellaneous settings ----- - SWAGGER_UI_DARK_MODE: bool = False DEVELOPMENT_MODE: bool = False diff --git a/notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb b/notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb index 3d4a74c..d33a9a6 100644 --- a/notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb +++ b/notebooks/experiments/anonymization/05-entity-grouping/entitty-groups.ipynb @@ -15,8 +15,8 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", "import json\n", + "import os\n", "\n", "import requests\n", "from tqdm import tqdm\n", @@ -58,7 +58,7 @@ " # Open the file in binary mode and send the POST request\n", " with open(file_path, \"rb\") as file:\n", " files = {\"file\": file}\n", - " response = requests.post(url=f\"{API_URL}/document-extract\", files=files)\n", + " response = requests.post(url=f\"{API_URL}/document/extract\", files=files)\n", " response.raise_for_status()\n", " return response.json()" ] @@ -80,7 +80,11 @@ "metadata": {}, "outputs": [], "source": [ - "len(extracted_document[\"document\"])" + "len(extracted_document[\"paragraphs\"])\n", + "\n", + "document_id = extracted_document[\"id\"]\n", + "print(\"document_id:\", document_id)\n", + "print(\"number of paragraphs:\", len(extracted_document[\"paragraphs\"]))" ] }, { @@ -96,9 +100,15 @@ "metadata": {}, "outputs": [], "source": [ + "import uuid\n", + "\n", + "\n", "# Function to make inference using the API\n", - "def get_predictions(sample: str) -> dict:\n", - " response = requests.post(url=f\"{API_URL}/anonymizer/predict\", json={\"text\": sample})\n", + "def get_predictions(paragraph_id: uuid.UUID) -> dict:\n", + " response = requests.get(\n", + " url=f\"{API_URL}/pipeline/anonymization/paragraph/{paragraph_id}/predict\",\n", + " params={\"use_cache\": True},\n", + " )\n", " response.raise_for_status()\n", " return response.json()" ] @@ -110,7 +120,8 @@ "outputs": [], "source": [ "predictions = [\n", - " get_predictions(paragraph) for paragraph in tqdm(extracted_document[\"document\"])\n", + " get_predictions(paragraph[\"id\"])\n", + " for paragraph in tqdm(extracted_document[\"paragraphs\"])\n", "]\n", "predictions" ] @@ -121,7 +132,45 @@ "metadata": {}, "outputs": [], "source": [ - "predictions[0]" + "predictions[5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aymurai.database.crud.prediction import read_document_prediction_paragraphs\n", + "from aymurai.database.meta.extra import ParagraphPredictionPublic\n", + "from aymurai.database.schema import ModelType\n", + "from aymurai.database.session import get_session\n", + "\n", + "session = next(get_session())\n", + "\n", + "annotations = read_document_prediction_paragraphs(\n", + " session=session,\n", + " document_id=uuid.UUID(document_id),\n", + " model_type=ModelType.ANONYMIZATION,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "annotations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "document_id\n" ] }, { @@ -132,22 +181,25 @@ "source": [ "from itertools import groupby\n", "\n", + "from more_itertools import flatten\n", + "\n", "\n", "def get_entities(prediction):\n", " return prediction[\"labels\"]\n", "\n", "\n", - "# entities = [entity for prediction in predictions for entity in get_entities(prediction)]\n", - "# entities = sorted(entities, key=lambda x: x[\"attrs\"][\"aymurai_label\"])\n", + "labels = [para.prediction.labels for para in annotations if para.prediction]\n", + "entities = list(flatten(labels))\n", "\n", - "entities = [entity for prediction in predictions for entity in get_entities(prediction)]\n", - "entities = [(i, entity) for i, entity in enumerate(entities)]\n", - "entities = sorted(entities, key=lambda x: x[1][\"attrs\"][\"aymurai_label\"])\n", + "indexed_entities = [(i, entity) for i, entity in enumerate(entities)]\n", + "indexed_entities = sorted(indexed_entities, key=lambda x: x[1].attrs.aymurai_label)\n", "\n", "\n", "groups = {\n", " label: list(group)\n", - " for label, group in groupby(entities, key=lambda x: x[1][\"attrs\"][\"aymurai_label\"])\n", + " for label, group in groupby(\n", + " indexed_entities, key=lambda x: x[1].attrs.aymurai_label\n", + " )\n", "}\n", "groups" ] @@ -158,20 +210,7 @@ "metadata": {}, "outputs": [], "source": [ - "import jiwer\n", - "\n", - "group = groups[\"PER\"]\n", - "example = group[0]\n", - "# other = [ent[\"text\"] for ent in group[1:]]\n", - "other = [ent[1][\"text\"] for ent in group[1:]]\n", - "\n", - "display(example)\n", - "\n", - "scores = {\n", - " \"cer\": [jiwer.cer(example[1][\"text\"], text) for text in other],\n", - " \"wer\": [jiwer.wer(example[1][\"text\"], text) for text in other],\n", - " \"mer\": [jiwer.mer(example[1][\"text\"], text) for text in other],\n", - "}\n" + "groups.keys()" ] }, { @@ -180,80 +219,7 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "import regex\n", - "import unicodedata\n", - "from jarowinkler import jarowinkler_similarity\n", - "\n", - "\n", - "def normalize_text(text):\n", - " # normalize tildes\n", - " text = unicodedata.normalize(\"NFKD\", text)\n", - " text = \"\".join(char for char in text if unicodedata.category(char) != \"Mn\")\n", - "\n", - " # remove extra spaces and special characters\n", - " text = regex.sub(r\"\\s+\", \" \", text)\n", - " text = regex.sub(r\"\\p{P}\", \"\", text)\n", - "\n", - " # lowercase\n", - " text = text.lower()\n", - "\n", - " return text\n", - "\n", - "\n", - "def compute_norm_cer(x, y):\n", - " x = normalize_text(x)\n", - " y = normalize_text(y)\n", - " return jiwer.cer(x, y) / len(x)\n", - "\n", - "\n", - "def compute_jaro_winkler(x, y):\n", - " x = normalize_text(x)\n", - " y = normalize_text(y)\n", - " return jarowinkler_similarity(x.split(), y.split())\n", - "\n", - "\n", - "def compute_word_subset(x, y):\n", - " x = normalize_text(x)\n", - " y = normalize_text(y)\n", - "\n", - " x = set(x.split())\n", - " y = set(y.split())\n", - " return bool(x & y)\n", - "\n", - "\n", - "scores = {\n", - " \"cer\": np.array(\n", - " [\n", - " [\n", - " compute_norm_cer(sample1[1][\"text\"], sample2[1][\"text\"])\n", - " for sample2 in group\n", - " ]\n", - " for sample1 in group\n", - " ]\n", - " ),\n", - " \"jaro_winkler\": np.array(\n", - " [\n", - " [\n", - " compute_jaro_winkler(sample1[1][\"text\"], sample2[1][\"text\"])\n", - " for sample2 in group\n", - " ]\n", - " for sample1 in group\n", - " ]\n", - " ),\n", - " \"word_subset\": 1\n", - " - np.array(\n", - " [\n", - " [\n", - " compute_word_subset(sample1[1][\"text\"], sample2[1][\"text\"])\n", - " for sample2 in group\n", - " ]\n", - " for sample1 in group\n", - " ]\n", - " ),\n", - "}\n", - "\n", - "scores[\"jaro_winkler\"].shape, scores[\"word_subset\"].shape" + "entities" ] }, { @@ -262,56 +228,100 @@ "metadata": {}, "outputs": [], "source": [ + "import unicodedata\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import regex\n", "from sklearn.cluster import DBSCAN\n", + "from sklearn.feature_extraction.text import CountVectorizer\n", "\n", - "dbscan = DBSCAN(eps=0.01, min_samples=2)\n", + "from aymurai.meta.entities import DocLabel\n", "\n", - "clusters = dbscan.fit_predict(scores[\"word_subset\"])\n", - "clusters\n", "\n", - "labels = set(clusters)\n", - "labels.discard(-1)\n", - "centroids = [\n", - " \" \".join(\n", - " [normalize_text(group[i][1][\"text\"]) for i in np.where(clusters == label)[0]]\n", + "# Normalize helper\n", + "def normalize_text(text):\n", + " text = unicodedata.normalize(\"NFKD\", text)\n", + " text = \"\".join(ch for ch in text if unicodedata.category(ch) != \"Mn\")\n", + " text = regex.sub(r\"\\s+\", \" \", text)\n", + " text = regex.sub(r\"\\p{P}\", \"\", text)\n", + " return text.lower()\n", + "\n", + "\n", + "def cluster_entities(entities: list[DocLabel], eps: float = 0.01, min_samples: int = 2):\n", + " \"\"\"\n", + " Cluster entity texts in a group and return a DataFrame with columns:\n", + " text, norm_text, index, cluster. Uses binary vectorization and Jaccard-DBSCAN.\n", + " \"\"\"\n", + "\n", + " indexed_entities = [(i, entity) for i, entity in enumerate(entities)]\n", + " indexed_entities = sorted(indexed_entities, key=lambda x: x[1].attrs.aymurai_label)\n", + "\n", + " # Prepare raw and normalized texts\n", + " texts = [ent[1].text for ent in indexed_entities]\n", + " norm_texts = [normalize_text(t) for t in texts]\n", + "\n", + " # --------- Binary vectorization of words ----------------\n", + " vectorizer = CountVectorizer(binary=True, token_pattern=r\"\\b\\w+\\b\")\n", + " matrix = vectorizer.fit_transform(\n", + " norm_texts\n", + " ) # sparse matrix (n_samples x n_features)\n", + "\n", + " # --------- Jaccard distance matrix ----------------\n", + " ints = (matrix @ matrix.T).toarray()\n", + " row_sums = matrix.sum(axis=1).A1\n", + " union = row_sums[:, None] + row_sums[None, :] - ints\n", + " # Avoid division by zero and compute distances\n", + " with np.errstate(divide=\"ignore\", invalid=\"ignore\"):\n", + " X = 1 - (ints / union)\n", + " X[union == 0] = 1.0\n", + "\n", + " # -------- DBSCAN ----------------------------------------------------\n", + " db = DBSCAN(eps=eps, min_samples=min_samples, metric=\"precomputed\")\n", + " labels = db.fit_predict(X)\n", + "\n", + " # Merge clusters by overlapping centroids\n", + " unique_lbls = sorted(set(labels) - {-1})\n", + " if unique_lbls:\n", + " # build centroid binary vectors\n", + " centroid_vectors = []\n", + " for lbl in unique_lbls:\n", + " mat = matrix[labels == lbl]\n", + " centroid = (mat.sum(axis=0) > 0).A1 # boolean mask\n", + " centroid_vectors.append(centroid)\n", + "\n", + " C = np.vstack(centroid_vectors).astype(bool)\n", + " sim = (C.astype(int) @ C.T.astype(int)) > 0\n", + " mapping = {\n", + " lbl: unique_lbls[int(np.argmax(sim[idx]))]\n", + " for idx, lbl in enumerate(unique_lbls)\n", + " }\n", + " labels = [mapping.get(lbl, -1) for lbl in labels]\n", + "\n", + " # Build results DataFrame\n", + " df = (\n", + " pd.DataFrame({\n", + " \"id\": [ent[1].id for ent in indexed_entities],\n", + " \"index\": [ent[0] for ent in indexed_entities],\n", + " \"paragraph_id\": [ent[1].fk_paragraph for ent in indexed_entities],\n", + " \"label\": [ent[1].attrs.aymurai_label for ent in indexed_entities],\n", + " \"text\": texts,\n", + " \"norm_text\": norm_texts,\n", + " \"cluster\": labels,\n", + " })\n", + " .sort_values(\"cluster\")\n", + " .reset_index(drop=True)\n", " )\n", - " for label in labels\n", - "]\n", - "centroids\n", - "\n", - "centroids_adj = np.array(\n", - " [[compute_word_subset(c1, c2) for c2 in centroids] for c1 in centroids]\n", - ")\n", - "centroids_adj\n", - "new_labels = np.argmax(centroids_adj, axis=-1)\n", - "\n", - "clusters = np.array([new_labels[label] if label != -1 else -1 for label in clusters])\n", - "clusters" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "\n", - "results = pd.DataFrame(\n", - " {\n", - " \"text\": [group[i][1][\"text\"] for i in range(len(clusters))],\n", - " \"norm_text\": [\n", - " normalize_text(group[i][1][\"text\"]) for i in range(len(clusters))\n", - " ],\n", - " \"index\": [group[i][0] for i in range(len(clusters))],\n", - " \"cluster\": clusters,\n", - " }\n", - ")\n", + " df[\"id\"] = df[\"id\"].apply(uuid.UUID)\n", + " df[\"paragraph_id\"] = df[\"paragraph_id\"].apply(uuid.UUID)\n", + " df.set_index(\"id\", inplace=True)\n", "\n", - "results = results.sort_values(\"cluster\")\n", + " return df\n", "\n", "\n", - "results.groupby(\"cluster\").apply(lambda x: x[\"norm_text\"].tolist()).to_dict()" + "# Example: cluster the PER group\n", + "results = cluster_entities(entities)\n", + "results" ] }, { @@ -320,15 +330,8 @@ "metadata": {}, "outputs": [], "source": [ - "results" + "results.groupby(\"cluster\").apply(lambda x: x[\"norm_text\"].tolist()).to_dict()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/notebooks/experiments/pdf-support/04-anonymize-pdf.ipynb b/notebooks/experiments/pdf-support/04-anonymize-pdf.ipynb index 2bb1a47..83fe365 100644 --- a/notebooks/experiments/pdf-support/04-anonymize-pdf.ipynb +++ b/notebooks/experiments/pdf-support/04-anonymize-pdf.ipynb @@ -15,20 +15,25 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", "import json\n", + "import os\n", "\n", "import requests\n", + "from rich.console import Console\n", "from tqdm import tqdm\n", "\n", + "console = Console(width=100)\n", + "\n", "API_URL = \"http://localhost:8999\" # Url for debugger. change it to your own" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "## Sample document" + "print(f\"API URL: {API_URL}\")" ] }, { @@ -37,7 +42,7 @@ "metadata": {}, "outputs": [], "source": [ - "doc_path = \"/resources/data/sample/document-01.pdf\"" + "doc_path = \"/resources/data/sample/document-03.docx\"" ] }, { @@ -58,7 +63,7 @@ " # Open the file in binary mode and send the POST request\n", " with open(file_path, \"rb\") as file:\n", " files = {\"file\": file}\n", - " response = requests.post(url=f\"{API_URL}/document-extract\", files=files)\n", + " response = requests.post(url=f\"{API_URL}/document/extract\", files=files)\n", " response.raise_for_status()\n", " return response.json()" ] @@ -71,7 +76,7 @@ "source": [ "# /document-extract endpoint output\n", "extracted_document = extract_document(doc_path)\n", - "extracted_document" + "console.print(extracted_document)" ] }, { @@ -80,7 +85,7 @@ "metadata": {}, "outputs": [], "source": [ - "len(extracted_document[\"document\"])" + "len(extracted_document[\"paragraphs\"])" ] }, { @@ -96,9 +101,15 @@ "metadata": {}, "outputs": [], "source": [ + "import uuid\n", + "\n", + "\n", "# Function to make inference using the API\n", - "def get_predictions(sample: str) -> dict:\n", - " response = requests.post(url=f\"{API_URL}/anonymizer/predict\", json={\"text\": sample})\n", + "def get_predictions(paragraph_id: uuid.UUID) -> dict:\n", + " response = requests.get(\n", + " url=f\"{API_URL}/pipeline/anonymization/paragraph/{paragraph_id}/predict\",\n", + " params={\"use_cache\": True},\n", + " )\n", " response.raise_for_status()\n", " return response.json()" ] @@ -110,7 +121,8 @@ "outputs": [], "source": [ "predictions = [\n", - " get_predictions(paragraph) for paragraph in tqdm(extracted_document[\"document\"])\n", + " get_predictions(paragraph[\"id\"])\n", + " for paragraph in tqdm(extracted_document[\"paragraphs\"])\n", "]\n", "predictions" ] @@ -121,32 +133,14 @@ "metadata": {}, "outputs": [], "source": [ - "json_prediction = json.dumps({\"data\": predictions})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with open(doc_path, \"rb\") as file:\n", - " files = {\"file\": file}\n", + "document_id = extracted_document[\"id\"]\n", + "\n", + "response = requests.get(\n", + " url=f\"{API_URL}/pipeline/anonymization/document/{document_id}/compile\",\n", + ")\n", + "response.raise_for_status()\n", + "\n", "\n", - " response = requests.post(\n", - " url=f\"{API_URL}/anonymizer/anonymize-document\",\n", - " data={\"annotations\": json_prediction},\n", - " files=files,\n", - " )\n", - " response.raise_for_status()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ "output_dir = \"output\"\n", "os.makedirs(output_dir, exist_ok=True)\n", "\n", diff --git a/notebooks/experiments/pdf-support/05-datapublic.ipynb b/notebooks/experiments/pdf-support/05-datapublic.ipynb index 214d515..3177272 100644 --- a/notebooks/experiments/pdf-support/05-datapublic.ipynb +++ b/notebooks/experiments/pdf-support/05-datapublic.ipynb @@ -55,7 +55,7 @@ " # Open the file in binary mode and send the POST request\n", " with open(file_path, \"rb\") as file:\n", " files = {\"file\": file}\n", - " response = requests.post(url=f\"{API_URL}/document-extract\", files=files)\n", + " response = requests.post(url=f\"{API_URL}/document/extract\", files=files)\n", " response.raise_for_status()\n", " return response.json()" ] diff --git a/resources/api/static/logo256-text.ico b/resources/api/static/logo256-text.ico deleted file mode 100644 index 9ff327f6d05389074da828615bd4afcf200f5945..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1711 zcmd^=|3A|S9LGPC2)TG%i*&b9W6hNu;j-dGm=9rLzLg14lXY3XjD_J4uBMN##bc{k zL|U>U^Chc0?s)hTy7Fz5d>OXYsfm0!{*CVae!O0f=i~8y{qXwj4FHIZ-5dm<58PA% zfL$A$MRX@GW)VP-#G9 zj2!pYx~o*Ge#zLh5@_52tsmbY^^LE3A-GBdDT?+zP-v`iOKd4(}{wTBa3Z<;nglNzoCA ze`tFl$CwC2XC0N=_S$m(;2F)JGP<_ZsC$K(Kn4~P=KSO`C*-3s z6d&8OAKNc9(hZ=1pMOo^=JP0KH+bBd=aszkA$4}Ni&#=TUae>{h?EpSf_b#q)}v&J zt2SpE&JWjtd7ozxrp`>a6wYzZi!}NT=h3;B+%DLuy;`ui>N?0iP-V4ecq;5l0N)Vj zWqrf?YXy=x1s`@h&bd72V3zZJ!KupKNZ6!o*!N^+RedJ7lsG*w`|SK|3s%`Lqw;JI zh!kg8j#$04ki^IJzn8r|hY##Vqhx*e_`a1GLyBH^;YJPkL7H#q*6lZyv2cF0;C1)m zVuyCYMcN6@tS}G^dKDwM?KW1bY}h3ms+0EJd#5N%=k=dReL2I%c$#A~_MXxk>dBFn zm++eHCH4^+9`RtBua!R|vWzMwS;iVzf=r8e1B;$SfyrZBFfJWuDy3m#=|)g(Nj~Zg zd&$iBKE2PvYBLCG$8@9qC5?1)J&Q}qN00sO&RTwAm35tpj^4eIAPfXCn+vL=!YT)( zS0l+nk}hHa^#rxQwQVSVFVf$&QPngTL#i$aShf*KMwVYyU z_r`r^I^6%h0d_NjI^Zh*aSobHi5M+$^<1LL!|HZq`{)Gw6Mw&{!cebC%;op<_pckp z1i76y!BCM@}Mf)+(pquwu{AAZQ#4$9Y(==xp?R0!WzfQhJVFkQ^ z%U6~q{TIGYoL3VLyR3D%5q5o;SnN)p{E$#_2){>EKe*&0P3Un{RR8G#hL#PnU6|4| zc8u(3_G^V9zNYO`%sKrAwXvdAr{bj8j#Rleo|~jpcqqoim3t*P?J1r(79N#1B>FTR z*+Ui?HBHZ5Ft-XJe7b|WGKaD$g!?kZi^ykH?>YJlzinTgm?0P5#T;B7D-NrWHS)?> z%!me9MH9&0cbzGXBZV1KdVw$KZ8K}`*774;;T4^A1KJ}lBVFB@e_802_{$XL>A_^& z5eg)tI1gnfHiPUF%auYfIL_g{O~Y!^XEX0?hbtWGpYXp DBCgg( diff --git a/uv.lock b/uv.lock index 12b9f1f..e6b68e5 100644 --- a/uv.lock +++ b/uv.lock @@ -239,7 +239,7 @@ wheels = [ [[package]] name = "aymurai" -version = "1.1.7rc2.dev12+gedb4ea0.d20250512" +version = "1.1.7rc2.dev14+g5ee8f3e.d20250524" source = { editable = "." } dependencies = [ { name = "alembic" }, From 686a8ba4c863c0a598a283dc1978b7df53276bfb Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Sun, 25 May 2025 05:52:57 +0000 Subject: [PATCH 08/11] =?UTF-8?q?chore(database):=20=F0=9F=94=A5=20remove?= =?UTF-8?q?=20unused=20anonymization=20and=20datapublic=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/crud/anonymization/__init__.py | 0 .../database/crud/anonymization/document.py | 77 ------------ .../database/crud/anonymization/paragraph.py | 101 ---------------- aymurai/database/crud/datapublic/__init__.py | 0 aymurai/database/crud/datapublic/document.py | 76 ------------ aymurai/database/crud/datapublic/paragraph.py | 112 ------------------ 6 files changed, 366 deletions(-) delete mode 100644 aymurai/database/crud/anonymization/__init__.py delete mode 100644 aymurai/database/crud/anonymization/document.py delete mode 100644 aymurai/database/crud/anonymization/paragraph.py delete mode 100644 aymurai/database/crud/datapublic/__init__.py delete mode 100644 aymurai/database/crud/datapublic/document.py delete mode 100644 aymurai/database/crud/datapublic/paragraph.py diff --git a/aymurai/database/crud/anonymization/__init__.py b/aymurai/database/crud/anonymization/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/aymurai/database/crud/anonymization/document.py b/aymurai/database/crud/anonymization/document.py deleted file mode 100644 index 13a1c02..0000000 --- a/aymurai/database/crud/anonymization/document.py +++ /dev/null @@ -1,77 +0,0 @@ -import uuid - -from sqlmodel import Session, select - -from aymurai.logger import get_logger -from aymurai.database.schema import ( - AnonymizationDocument, - AnonymizationParagraph, - AnonymizationDocumentUpdate, -) - -logger = get_logger(__name__) - - -def anonymization_document_create( - id: uuid.UUID, - name: str, - paragraphs: list[AnonymizationParagraph], - session: Session, - override: bool = False, -) -> AnonymizationDocument: - document = AnonymizationDocument(id=id, name=name, paragraphs=paragraphs) - - exists = session.get(AnonymizationDocument, id) - if exists and override: - session.delete(exists) - - session.add(document) - session.commit() - session.refresh(document) - - return document - - -def anonymization_document_read( - document_id: uuid.UUID, - session: Session, -) -> AnonymizationDocument | None: - statement = select(AnonymizationDocument).where( - AnonymizationDocument.id == document_id - ) - data = session.exec(statement).first() - return data - - -def anonymization_document_update( - document_id: uuid.UUID, - document_in: AnonymizationDocumentUpdate, - session: Session, -) -> AnonymizationDocument: - statement = select(AnonymizationDocument).where( - AnonymizationDocument.id == document_id - ) - document = session.exec(statement).first() - - if not document: - raise ValueError(f"Document not found: {document_id}") - - for field, value in document_in.model_dump(exclude_unset=True).items(): - setattr(document, field, value) - - return anonymization_document_create(document, session) - - -def anonymization_document_delete(document_id: uuid.UUID, session: Session): - statement = select(AnonymizationDocument).where( - AnonymizationDocument.id == document_id - ) - document = session.exec(statement).first() - - if not document: - raise ValueError(f"Document not found: {document_id}") - - session.delete(document) - session.commit() - - return diff --git a/aymurai/database/crud/anonymization/paragraph.py b/aymurai/database/crud/anonymization/paragraph.py deleted file mode 100644 index 593ebbd..0000000 --- a/aymurai/database/crud/anonymization/paragraph.py +++ /dev/null @@ -1,101 +0,0 @@ -import uuid - -from sqlmodel import Session - -from aymurai.database.schema import ( - AnonymizationParagraph, - AnonymizationParagraphCreate, - AnonymizationParagraphUpdate, -) -from aymurai.database.utils import text_to_uuid -from aymurai.logger import get_logger - -logger = get_logger(__name__) - - -def anonymization_paragraph_create( - paragraph_in: AnonymizationParagraphCreate, - session: Session, - override: bool = False, -) -> AnonymizationParagraph: - new_paragraph = AnonymizationParagraph(**paragraph_in.model_dump()) - - if override: - existing = session.get(AnonymizationParagraph, new_paragraph.id) - - if existing: - session.delete(existing) - - session.add(new_paragraph) - session.commit() - session.refresh(new_paragraph) - return new_paragraph - - -def anonymization_paragraph_read( - paragraph_id: uuid.UUID, - session: Session, -) -> AnonymizationParagraph | None: - return session.get(AnonymizationParagraph, paragraph_id) - - -def anonymization_paragraph_update( - paragraph_id: uuid.UUID, - paragraph_in: AnonymizationParagraphUpdate, - session: Session, -) -> AnonymizationParagraph: - paragraph = session.get(AnonymizationParagraph, paragraph_id) - - if not paragraph: - raise ValueError(f"Paragraph not found: {paragraph_id}") - - for field, value in paragraph_in.model_dump(exclude_none=True).items(): - setattr(paragraph, field, value) - - session.add(paragraph) - session.commit() - session.refresh(paragraph) - return paragraph - - -def anonymization_paragraph_delete(paragraph_id: uuid.UUID, session: Session): - paragraph = session.get(AnonymizationParagraph, paragraph_id) - - if not paragraph: - raise ValueError(f"Paragraph not found: {paragraph_id}") - - session.delete(paragraph) - session.commit() - - return - - -# FIXME: This can be CLEARLY optimized -def anonymization_paragraph_batch_create_update( - paragraphs_in: list[AnonymizationParagraphCreate], session: Session -) -> list[AnonymizationParagraph]: - paragraphs = [] - - for p_in in paragraphs_in: - paragraph_id = text_to_uuid(p_in.text) - - paragraph = session.get(AnonymizationParagraph, paragraph_id) - if paragraph: - update = AnonymizationParagraphUpdate(**p_in.model_dump()) - - for field, value in update.model_dump(exclude_none=True).items(): - setattr(paragraph, field, value) - - else: - paragraph = AnonymizationParagraph(**p_in.model_dump()) - - session.add(paragraph) - session.commit() - session.refresh(paragraph) - - paragraphs.append(paragraph) - - # refresh models (Must be a list or for-loop) - [session.refresh(paragraph) for paragraph in paragraphs] - - return paragraphs diff --git a/aymurai/database/crud/datapublic/__init__.py b/aymurai/database/crud/datapublic/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/aymurai/database/crud/datapublic/document.py b/aymurai/database/crud/datapublic/document.py deleted file mode 100644 index 293a163..0000000 --- a/aymurai/database/crud/datapublic/document.py +++ /dev/null @@ -1,76 +0,0 @@ -import uuid - -from sqlmodel import Session, select - -from aymurai.logger import get_logger -from aymurai.database.schema import ( - DataPublicDocument, - DataPublicParagraph, - DataPublicDocumentUpdate, -) - -logger = get_logger(__name__) - - -def datapublic_document_create( - id: uuid.UUID, - name: str, - paragraphs: list[DataPublicParagraph], - session: Session, - override: bool = False, -) -> DataPublicDocument: - document = DataPublicDocument(id=id, name=name, paragraphs=paragraphs) - - if override: - statement = select(DataPublicDocument).where( - DataPublicDocument.id == document.id - ) - existing = session.exec(statement).first() - - if existing: - session.delete(existing) - - session.add(document) - session.commit() - session.refresh(document) - - return document - - -def datapublic_document_read( - document_id: uuid.UUID, - session: Session, -) -> DataPublicDocument | None: - statement = select(DataPublicDocument).where(DataPublicDocument.id == document_id) - data = session.exec(statement).first() - return data - - -def datapublic_document_update( - document_id: uuid.UUID, - document_in: DataPublicDocumentUpdate, - session: Session, -) -> DataPublicDocument: - statement = select(DataPublicDocument).where(DataPublicDocument.id == document_id) - document = session.exec(statement).first() - - if not document: - raise ValueError(f"Document not found: {document_id}") - - for field, value in document_in.model_dump(exclude_unset=True).items(): - setattr(document, field, value) - - return datapublic_document_create(document, session) - - -def datapublic_document_delete(document_id: uuid.UUID, session: Session): - statement = select(DataPublicDocument).where(DataPublicDocument.id == document_id) - document = session.exec(statement).first() - - if not document: - raise ValueError(f"Document not found: {document_id}") - - session.delete(document) - session.commit() - - return diff --git a/aymurai/database/crud/datapublic/paragraph.py b/aymurai/database/crud/datapublic/paragraph.py deleted file mode 100644 index 591334a..0000000 --- a/aymurai/database/crud/datapublic/paragraph.py +++ /dev/null @@ -1,112 +0,0 @@ -import uuid - -from sqlmodel import Session, select - -from aymurai.database.schema import ( - DataPublicParagraph, - DataPublicParagraphCreate, - DataPublicParagraphUpdate, -) -from aymurai.database.utils import text_to_uuid -from aymurai.logger import get_logger - -logger = get_logger(__name__) - - -def datapublic_paragraph_create( - paragraph_in: DataPublicParagraphCreate, - session: Session, - override: bool = False, -) -> DataPublicParagraph: - paragraph = DataPublicParagraph(**paragraph_in.model_dump()) - - if override: - statement = select(DataPublicParagraph).where( - DataPublicParagraph.id == paragraph.id - ) - existing = session.exec(statement).first() or paragraph - - if existing: - session.delete(existing) - - session.add(paragraph) - session.commit() - session.refresh(paragraph) - return paragraph - - -def datapublic_paragraph_read( - paragraph_id: uuid.UUID, - session: Session, -) -> DataPublicParagraph | None: - statement = select(DataPublicParagraph).where( - DataPublicParagraph.id == paragraph_id - ) - data = session.exec(statement).first() - return data - - -def datapublic_paragraph_update( - paragraph_id: uuid.UUID, - paragraph_in: DataPublicParagraphUpdate, - session: Session, -) -> DataPublicParagraph: - statement = select(DataPublicParagraph).where( - DataPublicParagraph.id == paragraph_id - ) - paragraph = session.exec(statement).first() - - if not paragraph: - raise ValueError(f"Paragraph not found: {paragraph_id}") - - for field, value in paragraph_in.model_dump(exclude_none=True).items(): - setattr(paragraph, field, value) - - return datapublic_paragraph_create(paragraph, session) - - -def datapublic_paragraph_delete(paragraph_id: uuid.UUID, session: Session): - statement = select(DataPublicParagraph).where( - DataPublicParagraph.id == paragraph_id - ) - paragraph = session.exec(statement).first() - - if not paragraph: - raise ValueError(f"Paragraph not found: {paragraph_id}") - - session.delete(paragraph) - session.commit() - - return - - -def datapublic_paragraph_batch_create_update( - paragraphs_in: list[DataPublicParagraphCreate], session: Session -) -> list[DataPublicParagraph]: - paragraphs = [] - - for paragraph_in in paragraphs_in: - paragraph_id = text_to_uuid(paragraph_in.text) - - statement = select(DataPublicParagraph).where( - DataPublicParagraph.id == paragraph_id - ) - paragraph = session.exec(statement).first() - if paragraph: - update = DataPublicParagraphUpdate(**paragraph_in.model_dump()) - - for field, value in update.model_dump(exclude_none=True).items(): - setattr(paragraph, field, value) - - else: - paragraph = DataPublicParagraph(**paragraph_in.model_dump()) - - paragraphs.append(paragraph) - - session.add(paragraph) - session.commit() - - # refresh models (Must be a list or for-loop) - [session.refresh(paragraph) for paragraph in paragraphs] - - return paragraphs From 0d87dd471bd00e9ff1d84e650b5e794f6f91e505 Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Tue, 17 Jun 2025 04:50:15 +0000 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=90=9B=20fix(extract):=20=20impleme?= =?UTF-8?q?nt=20concurrent=20text=20extraction=20to=20prevent=20blocking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added concurrent processing for text extraction to enhance performance. - Introduced a timeout mechanism to handle long-running extraction tasks. - Removed unnecessary threading lock for cleaner code. --- .../api/endpoints/routers/document/extract.py | 61 +++++++++++++++---- aymurai/api/main.py | 3 - 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/aymurai/api/endpoints/routers/document/extract.py b/aymurai/api/endpoints/routers/document/extract.py index 98477e9..2723211 100644 --- a/aymurai/api/endpoints/routers/document/extract.py +++ b/aymurai/api/endpoints/routers/document/extract.py @@ -1,12 +1,13 @@ +import concurrent.futures import os import re import tempfile -import threading -from fastapi import Depends, UploadFile +from fastapi import Depends, HTTPException, UploadFile from fastapi.routing import APIRouter from more_itertools import unique_justseen from sqlmodel import Session +from starlette import status from aymurai.api.utils import get_pipeline_doc_extract from aymurai.database.schema import Document, DocumentPublic, Paragraph @@ -14,15 +15,46 @@ from aymurai.database.utils import data_to_uuid from aymurai.logger import get_logger from aymurai.pipeline import AymurAIPipeline -from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER -from aymurai.utils.misc import get_element +from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER, extract_document +from aymurai.text.normalize import document_normalize logger = get_logger(__name__) -lock = threading.Lock() router = APIRouter() +def extraction(path: str) -> str: + """ + Wrapper function to call the extract_document function. + This is necessary to ensure that the function can be pickled and run in a separate process. + """ + text = extract_document(path) + return document_normalize(text) if text else "" + + +def run_safe_text_extraction(path: str, timeout_s: float = 5) -> str: + """ + Runs the text extraction in a separate process to avoid blocking the main thread. + This is useful for long-running tasks or when the extraction might hang. + Args: + path (str): Path to the file to be processed. + timeout_s (float): Timeout in seconds for the extraction process. + Returns: + str: Extracted text from the document. + Raises: + TimeoutError: If the extraction process exceeds the specified timeout. + """ + + with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: + future = executor.submit(extraction, path) + try: + return future.result(timeout=timeout_s) + except concurrent.futures.TimeoutError: + # Cancel/killing the subprocess + future.cancel() + raise + + @router.post("/extract", response_model=DocumentPublic) def plain_text_extractor( file: UploadFile, @@ -58,19 +90,26 @@ def plain_text_extractor( logger.info("processing data item") logger.info(f"{item}") - with lock: - processed = pipeline.preprocess([item]) + document = run_safe_text_extraction(tmp_filename, timeout_s=5) + + except concurrent.futures.TimeoutError: + logger.error(f"Timeout while extracting text from {file.filename}") + raise HTTPException( + status_code=status.HTTP_504_GATEWAY_TIMEOUT, + detail="Text extraction timed out", + ) except Exception as e: logger.error(f"error while processing data item: {e}") - raise e + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) os.remove(tmp_filename) logger.info(f"removed temp file from local storage => {tmp_filename}") - doc_text: str = get_element(processed[0], ["data", "doc.text"], "") - - paragraph_text = [text.strip() for text in doc_text.split("\n") if text.strip()] + paragraph_text = [text.strip() for text in document.split("\n") if text.strip()] paragraph_text = [re.sub(r"\s{2,}", " ", text) for text in paragraph_text] paragraph_text = list(unique_justseen(paragraph_text)) diff --git a/aymurai/api/main.py b/aymurai/api/main.py index e113070..800b208 100644 --- a/aymurai/api/main.py +++ b/aymurai/api/main.py @@ -135,9 +135,6 @@ def environment(): if __name__ == "__main__": # download the necessary data logger.info("Loading pipelines and exit.") - AymurAIPipeline.load( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "doc-extraction") - ) AymurAIPipeline.load( os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") ) From 345cf8448099bab9e044ebd7faf722a701c26864 Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Tue, 17 Jun 2025 05:49:25 +0000 Subject: [PATCH 10/11] =?UTF-8?q?chore(docker):=20=F0=9F=94=A7=20remove=20?= =?UTF-8?q?unused=20dependencies=20and=20streamline=20Docker=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed `libmagic1` from the Dockerfile to reduce image size. - Cleaned up environment variable configurations in docker-compose files. - Updated build process in GitHub Actions for better tag handling. --- .devcontainer/Dockerfile | 2 - .devcontainer/docker-compose.yml | 1 - .github/workflows/build-docker-image.yml | 53 ++++++++++++------- .../api/endpoints/routers/document/extract.py | 10 ++-- aymurai/models/decision/binregex.py | 3 +- aymurai/text/extraction.py | 13 ++--- docker-compose.yml | 3 -- docker/api/Dockerfile | 19 +++---- pyproject.toml | 1 - uv.lock | 13 +---- 10 files changed, 56 insertions(+), 62 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6fbb8c5..b459de2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -80,8 +80,6 @@ RUN apt update && apt install -y \ default-jre \ # Pandoc pandoc \ - # Libmagic - libmagic1 \ sqlite3 \ && apt-get autoremove -y \ && apt-get clean \ diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 5e12b20..1feb29c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -10,7 +10,6 @@ x-template: &template - /var/run/docker.sock:/var/run/docker.sock env_file: - ../.env.common - - ../.env command: /bin/sh -c "while true; do :; done" services: diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 3d9af7e..1101414 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -15,6 +15,17 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Extract tag name + id: envs + run: | + TAG_NAME="${GITHUB_REF#refs/tags/}" + echo "TAG=${TAG_NAME}" >> $GITHUB_OUTPUT + if [[ "$TAG_NAME" == *"rc"* ]]; then + echo "SUFFIX=-rc" >> $GITHUB_OUTPUT + else + echo "SUFFIX=" >> $GITHUB_OUTPUT + fi + # Step 2: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry uses: docker/login-action@v2 @@ -23,22 +34,28 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Step 3: Build the Docker image - - name: Build Docker Image - run: | - touch .env - docker compose build aymurai-api - docker compose build aymurai-api-full - - # Step 4: Push the Docker image to GHCR - - name: Push Docker Image - run: | - docker tag ghcr.io/aymurai/api:latest ghcr.io/aymurai/api:${{ github.ref_name }} - docker tag ghcr.io/aymurai/api:full ghcr.io/aymurai/api:full-${{ github.ref_name }} + - name: Build and push Docker image (aymurai-api) + uses: docker/build-push-action@v5 + with: + context: . + file: build/api/Dockerfile + push: true + target: aymurai-api + build-args: | + SETUPTOOLS_SCM_PRETEND_VERSION=${{ steps.envs.outputs.TAG }} + tags: | + ghcr.io/aymurai/api:latest${{ steps.envs.outputs.SUFFIX }} + ghcr.io/aymurai/api:${{ github.ref_name }} - docker push ghcr.io/aymurai/api:full-${{ github.ref_name }} - docker push ghcr.io/aymurai/api:${{ github.ref_name }} - if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - docker push ghcr.io/aymurai/api:latest - docker push ghcr.io/aymurai/api:full - fi + - name: Build and push Docker image (aymurai-api-full) + uses: docker/build-push-action@v5 + with: + context: . + file: build/api/Dockerfile + push: true + target: aymurai-api-full + build-args: | + SETUPTOOLS_SCM_PRETEND_VERSION=${{ steps.envs.outputs.TAG }} + tags: | + ghcr.io/aymurai/api:full${{ steps.envs.outputs.SUFFIX }} + ghcr.io/aymurai/api:${{ github.ref_name }}-full diff --git a/aymurai/api/endpoints/routers/document/extract.py b/aymurai/api/endpoints/routers/document/extract.py index 2723211..4147898 100644 --- a/aymurai/api/endpoints/routers/document/extract.py +++ b/aymurai/api/endpoints/routers/document/extract.py @@ -9,13 +9,11 @@ from sqlmodel import Session from starlette import status -from aymurai.api.utils import get_pipeline_doc_extract from aymurai.database.schema import Document, DocumentPublic, Paragraph from aymurai.database.session import get_session from aymurai.database.utils import data_to_uuid from aymurai.logger import get_logger -from aymurai.pipeline import AymurAIPipeline -from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER, extract_document +from aymurai.text.extraction import extract_document from aymurai.text.normalize import document_normalize logger = get_logger(__name__) @@ -45,7 +43,7 @@ def run_safe_text_extraction(path: str, timeout_s: float = 5) -> str: TimeoutError: If the extraction process exceeds the specified timeout. """ - with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(extraction, path) try: return future.result(timeout=timeout_s) @@ -58,12 +56,12 @@ def run_safe_text_extraction(path: str, timeout_s: float = 5) -> str: @router.post("/extract", response_model=DocumentPublic) def plain_text_extractor( file: UploadFile, - pipeline: AymurAIPipeline = Depends(get_pipeline_doc_extract), session: Session = Depends(get_session), use_cache: bool = True, ) -> DocumentPublic: logger.info(f"receiving => {file.filename}") - extension = MIMETYPE_EXTENSION_MAPPER.get(file.content_type) + _, extension = os.path.splitext(file.filename) + extension = extension.lstrip(".").lower() logger.info(f"detected extension: {extension} ({file.content_type})") data = file.file.read() diff --git a/aymurai/models/decision/binregex.py b/aymurai/models/decision/binregex.py index a3b77a0..46a7c80 100644 --- a/aymurai/models/decision/binregex.py +++ b/aymurai/models/decision/binregex.py @@ -7,7 +7,8 @@ from unidecode import unidecode from aymurai.logger import get_logger -from aymurai.meta.api_interfaces import DocLabel, EntityAttributes +from aymurai.meta.api_interfaces import DocLabel +from aymurai.meta.entities import EntityAttributes from aymurai.meta.pipeline_interfaces import TrainModule from aymurai.meta.types import DataBlock, DataItem from aymurai.models.decision.conv1d import Conv1dTextClassifier diff --git a/aymurai/text/extraction.py b/aymurai/text/extraction.py index 0e00cd1..2ee5aaa 100644 --- a/aymurai/text/extraction.py +++ b/aymurai/text/extraction.py @@ -1,13 +1,12 @@ -import os import logging -import zipfile +import os import statistics import unicodedata +import zipfile from pathlib import Path from typing import Any from zipfile import BadZipFile -import magic import numpy as np import pymupdf import textract @@ -80,11 +79,6 @@ def __call__(self, item: dict) -> dict: return item -def get_extension(path: str) -> str: - mimetype = magic.from_file(path, mime=True) - return MIMETYPE_EXTENSION_MAPPER.get(mimetype, mimetype) - - def _load_xml_from_odt(path: str, xmlfile: str = "styles.xml") -> str: """ Load xml file inside an odt. @@ -239,7 +233,8 @@ def extract_document( if errors not in ERRORS: raise ValueError(f"errors argument must be in {ERRORS}") - ext = get_extension(filename) + _, ext = os.path.splitext(filename) + ext = ext.lstrip(".").lower() kwargs["extension"] = kwargs.get("extension", ext) kwargs["output_encoding"] = kwargs.get("output_encoding", "utf-8") diff --git a/docker-compose.yml b/docker-compose.yml index 3ce7c53..d6ac06a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,9 +7,6 @@ services: context: . dockerfile: ./docker/api/Dockerfile target: aymurai-api - env_file: - - .env - - .env.common volumes: - ./resources/cache:/resources/cache diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 6db18e4..05ab765 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,3 +1,6 @@ +############################################################################### +# Stage - aymurai builder +############################################################################### FROM python:3.10-slim AS builder # install git @@ -18,6 +21,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \ COPY aymurai /app/aymurai # Sync the package +ARG SETUPTOOLS_SCM_PRETEND_VERSION +ENV SETUPTOOLS_SCM_PRETEND_VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION} RUN --mount=type=bind,source=.git,target=.git \ --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ @@ -25,8 +30,9 @@ RUN --mount=type=bind,source=.git,target=.git \ uv sync --frozen --no-editable +############################################################################### # Stage - aymurai-api -# ------------------- +############################################################################### FROM python:3.10-slim AS aymurai-api ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 @@ -43,11 +49,9 @@ ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 # install libreoffice RUN apt update \ && apt-get install -y --no-install-recommends \ - # antiword \ libreoffice-writer \ libreoffice-common \ default-jre \ - libmagic1 \ && apt-get autoremove -y \ && apt-get clean \ && rm -rf /tmp/* /var/tmp/* \ @@ -56,23 +60,20 @@ RUN apt update \ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY --from=builder --chown=app:app /app/.venv /app/.venv -COPY resources/api/static /resources/api/static COPY resources/pipelines /resources/pipelines WORKDIR /app CMD uv run fastapi run --port 8899 .venv/lib/python3.10/site-packages/aymurai/api/main.py - - - +############################################################################### +# Stage - aymurai-api-full +############################################################################### FROM aymurai-api AS aymurai-api-full ENV TF_CPP_MIN_LOG_LEVEL=3 ENV TFHUB_CACHE_DIR=resources/cache/tfhub_modules ENV RESOURCES_BASEPATH=resources -# copy api resources -COPY ./resources/api resources/api # copy pipelines & run app startup to download models COPY ./resources/pipelines/production resources/pipelines/production diff --git a/pyproject.toml b/pyproject.toml index 3bee606..819a110 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ dependencies = [ # "scikit-learn==1.5.2", "jiwer==3.0.5", "datasets>=3.2.0", - "python-magic==0.4.27", "unidecode==1.3.8", "sentencepiece==0.2.0", # "flair @ git+https://github.com/flairNLP/flair@v0.13.1", diff --git a/uv.lock b/uv.lock index e6b68e5..05baac1 100644 --- a/uv.lock +++ b/uv.lock @@ -239,7 +239,7 @@ wheels = [ [[package]] name = "aymurai" -version = "1.1.7rc2.dev14+g5ee8f3e.d20250524" +version = "1.1.10.dev12+g0d87dd4.d20250617" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -263,7 +263,6 @@ dependencies = [ { name = "pymupdf4llm" }, { name = "pypandoc" }, { name = "python-dotenv" }, - { name = "python-magic" }, { name = "python-multipart" }, { name = "pytorch-lightning" }, { name = "requests" }, @@ -317,7 +316,6 @@ requires-dist = [ { name = "pymupdf4llm", specifier = ">=0.0.17" }, { name = "pypandoc", specifier = ">=1.15" }, { name = "python-dotenv", specifier = ">=1.0.1" }, - { name = "python-magic", specifier = "==0.4.27" }, { name = "python-multipart", specifier = ">=0.0.20" }, { name = "pytorch-lightning", specifier = "==1.8.3.post1" }, { name = "requests", specifier = ">=2.32.3" }, @@ -2557,15 +2555,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163 }, ] -[[package]] -name = "python-magic" -version = "0.4.27" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840 }, -] - [[package]] name = "python-multipart" version = "0.0.20" From b8909c56275edcffc5c22250eda4221a26c39e78 Mon Sep 17 00:00:00 2001 From: jedzill4 Date: Tue, 17 Jun 2025 06:07:33 +0000 Subject: [PATCH 11/11] =?UTF-8?q?ci(github):=20=F0=9F=91=B7=20fix=20Docker?= =?UTF-8?q?file=20path=20in=20build=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corrected the file path for the Dockerfile in the build-and-push steps. - Ensures the workflow uses the correct Dockerfile location for building images. --- .github/workflows/build-docker-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 1101414..dfd7c3a 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -38,7 +38,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: build/api/Dockerfile + file: docker/api/Dockerfile push: true target: aymurai-api build-args: | @@ -51,7 +51,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: build/api/Dockerfile + file: docker/api/Dockerfile push: true target: aymurai-api-full build-args: |