diff --git a/.dockerignore b/.dockerignore index d439e99..b11bcd5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,15 @@ -/tests -.flake8 -.tox +.git +.github/ +.venv +__pycache__/ +*.py[cod] +tests/ +docs/ +reports/ +*.db +.pytest_cache +.ruff_cache .coverage +.idea .pre-commit-config.yaml LICENSE -mypy.ini -.mypy_cache -.pytest_cache diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9de1c14..7e8acbe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,20 +1,15 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python +# This workflow installs dependencies, lints, type-checks, and tests the project +# with a single version of Python provisioned by uv from .python-version. +# For more information see: +# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python name: Build on: push: - branches: [ '*' ] - paths: - - 'src/**' - - 'tests/**' - + branches: [ main ] pull_request: - branches: [ "main", "dev" ] - paths: - - 'src/**' - - 'tests/**' + branches: [ '**' ] permissions: contents: read @@ -25,25 +20,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: enable-cache: true - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - name: Install dependencies - run: | - python -m pip install --upgrade pip - make dev - - name: Lint - run: | - make lint - - name: Test - run: | - make cover - - name: Build Image - run: | - docker build . --file Dockerfile --tag template:PR-${{ github.event.number }} + run: make dev + - name: Lint and type-check + run: make lint + - name: ADR registry check + run: make adr-check + - name: Test with coverage gate + run: make cover + - name: Build image + run: docker build . --file Dockerfile --tag template:${{ github.sha }} diff --git a/.gitignore b/.gitignore index 83352ab..36eb4c5 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,9 @@ local_settings.py db.sqlite3 db.sqlite3-journal +# SQLite databases (default DATABASE_URL writes ./cosmic-fastapi.db) +*.db + # Flask stuff: instance/ .webassets-cache diff --git a/.python-version b/.python-version index 0c7d5f5..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11.4 +3.13 diff --git a/Dockerfile b/Dockerfile index 7f7951c..d3fd192 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,43 +1,57 @@ -FROM python:3.11.4-slim AS builder +FROM python:3.13-slim AS builder ARG APP_DIR=/app -ARG ENV - -ENV ENV=${ENV} \ - PYTHONUNBUFFERED=1 \ +ENV PYTHONUNBUFFERED=1 \ PYTHONHASHSEED=random \ PYTHONFAULTHANDLER=1 \ - UV_PYTHON=python3.11 \ - UV_COMPILE_BYTE=1 \ - UV_LINK_MODE=copy \ - UVICORN_PORT=8000 \ - UVICORN_HOST=0.0.0.0 \ - UVICORN_RELOAD=0 + UV_PYTHON=python3.13 \ + UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy # Install uv. COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR ${APP_DIR} -# Copy dependency definition files +# Copy dependency definition files. COPY pyproject.toml uv.lock .python-version README.md ${APP_DIR}/ -# Build the virtual environment from the lock file, excluding dev dependencies -# This creates a self-contained .venv directory -# Install dependencies +# Build the virtual environment from the lock file, excluding dev dependencies. +# This creates a self-contained .venv directory. RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev - -# Copy the project into the image +# Copy the project into the image and sync it. COPY src ${APP_DIR}/src - -# Sync the project RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev -# Set the command to run the application using the Python from the venv -CMD ["uv" , "run", "python", "-m", "template.main"] + +FROM python:3.13-slim AS runtime + +ARG APP_DIR=/app + +ENV PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PYTHONFAULTHANDLER=1 \ + PATH="${APP_DIR}/.venv/bin:$PATH" \ + UVICORN_PORT=8000 \ + UVICORN_HOST=0.0.0.0 \ + UVICORN_RELOAD=0 + +# Create an unprivileged application user. +RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_DIR} app + +WORKDIR ${APP_DIR} + +# Copy the prebuilt virtual environment and the application source. +COPY --from=builder --chown=app:app ${APP_DIR}/.venv ${APP_DIR}/.venv +COPY --from=builder --chown=app:app ${APP_DIR}/src ${APP_DIR}/src + +USER app + +EXPOSE 8000 + +# Run the application using the Python from the prebuilt venv. +CMD ["python", "-m", "template.main"] diff --git a/Makefile b/Makefile index 0fffb1f..c4b3f6e 100644 --- a/Makefile +++ b/Makefile @@ -6,29 +6,25 @@ .PHONY: clean clean: ## Removes all build and test artifacts rm -f .coverage - rm -rf .mypy_cache + rm -f *.db rm -rf .pytest_cache + rm -rf .ruff_cache rm -rf dist rm -rf reports - rm -f requirements.txt - rm -rf $(SSAP_DIR) + find . -type d -name __pycache__ -exec rm -rf {} + .PHONY: dist-clean dist-clean: clean ## Removes all build and test artifacts and virtual environment rm -rf .venv .PHONY: install -install: ## Install dependencies - uv sync +install: ## Install runtime dependencies + uv sync --no-dev .PHONY: dev -dev: ## Install dev dependencies +dev: ## Install runtime and dev dependencies uv sync --dev -.PHONY: build -build: ## Creates a virtual environment - uv venv - .PHONY: test test: ## Executes tests cases uv run pytest diff --git a/README.md b/README.md index cd4c61f..4a16324 100644 --- a/README.md +++ b/README.md @@ -70,15 +70,33 @@ extensions such as optimistic locking, broker adapters, and the transactional ou Variables prefixed with `FASTAPI_` are used to configure the API UI. -| Name | Description | Default Value | -|-----------------------------|---------------------|------------------| -| FASTAPI_DEBUG | Debug Mode | False | -| FASTAPI_PROJECT_NAME | Swagger Title | API GATEWAY | -| FASTAPI_PROJECT_DESCRIPTION | Swagger Description | ... | -| FASTAPI_PROJECT_LICENSE | License info | ... | -| FASTAPI_PROJECT_CONTACT | Contact details | ... | -| FASTAPI_VERSION | Application Version | template.version | -| FASTAPI_DOCS_URL | Swagger Endpoint | /docs | +| Name | Description | Default Value | +|------------------------------|---------------------------------------------|--------------------------------------------------| +| FASTAPI_DEBUG | Debug Mode | False | +| FASTAPI_PROJECT_NAME | Swagger Title | Cosmic FastAPI | +| FASTAPI_PROJECT_DESCRIPTION | Swagger Description | This is a FastAPI template demo. | +| FASTAPI_PROJECT_LICENSE | License info (JSON object, see below) | `{"name": "MIT", "url": "..."}` | +| FASTAPI_PROJECT_CONTACT | Contact details (JSON object, see below) | `{"name": "Tomas Sanchez", "url": "...", ...}` | +| FASTAPI_VERSION | Application Version | from package metadata (`pyproject.toml`) | +| FASTAPI_DOCS_URL | Swagger Endpoint | /docs | +| FASTAPI_BACKEND_CORS_ORIGINS | Allowed CORS origins (JSON list) | `["http://localhost:3000", "http://localhost:8000"]` | + +`FASTAPI_PROJECT_LICENSE` and `FASTAPI_PROJECT_CONTACT` parse into `LicenseInfo` and `ContactInfo` models, so they must +be provided as JSON objects: + +```bash +FASTAPI_PROJECT_LICENSE='{"name": "MIT", "url": "https://mit-license.org/"}' +FASTAPI_PROJECT_CONTACT='{"name": "Tomas Sanchez", "url": "https://tomsanchez.com.ar", "email": "info@tomsanchez.com.ar"}' +``` + +`FASTAPI_BACKEND_CORS_ORIGINS` is a JSON list of allowed origins. The CORS middleware disables credentials whenever the +list contains the `"*"` wildcard, since reflecting any origin together with credentials is an unsafe posture: + +```bash +FASTAPI_BACKEND_CORS_ORIGINS='["https://app.example.com", "https://admin.example.com"]' +``` + +`FASTAPI_VERSION` defaults to the installed package version (single source of truth in `pyproject.toml`). Variables prefixed with `UVICORN_` are used to configure the server. @@ -311,7 +329,7 @@ pip install uv 1. Clone the repository ```bash - git clone "git@github.com/tomasanchez/cosmic-fastapi.git" + git clone "git@github.com:tomasanchez/cosmic-fastapi.git" ``` 2. Install dependencies diff --git a/docker-compose.yaml b/docker-compose.yaml index 10a442c..76b41f9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,20 +1,30 @@ services: app: - # This tells Docker Compose to build the image from the Dockerfile - # in the current directory. + # Build the image from the Dockerfile in the current directory. build: . # Name the container for easier identification. container_name: cosmic-fastapi-app - # This maps port 8000 on your local machine to port 8000 inside the container, - # allowing you to access the running application at http://localhost:8000. + # Map port 8000 on the host to port 8000 inside the container so the app is + # reachable at http://localhost:8000. ports: - "8000:8000" - # This mounts your local 'src' folder into the container at '/app/src'. - # This is the key to live-reloading: changes you make to your code locally - # will be immediately reflected inside the container. + # Mount the local 'src' folder into the container so code changes are + # reflected for Uvicorn's live-reload. volumes: - ./src:/app/src - # Override environment variables defined in the Dockerfile. - # Here, we enable Uvicorn's auto-reload feature for development. + # Persist the SQLite database file across container recreation. + - app-data:/app/data environment: - - ENV=development + # Enable Uvicorn auto-reload for local development. + - UVICORN_RELOAD=true + # Store the SQLite database on the persistent named volume. + - DATABASE_URL=sqlite+pysqlite:///./data/cosmic-fastapi.db + # Create the schema at startup so the demo works on first run. + # Production deployments must use Alembic migrations (make migrate) + # instead of auto-creating the schema. + - DATABASE_AUTO_CREATE_SCHEMA=true + # Allow the bundled frontend / local tools to call the API in development. + - FASTAPI_BACKEND_CORS_ORIGINS=["http://localhost:8000"] + +volumes: + app-data: diff --git a/docs/adr/0016-aggregate-persistence-write-back.md b/docs/adr/0016-aggregate-persistence-write-back.md new file mode 100644 index 0000000..4659ea1 --- /dev/null +++ b/docs/adr/0016-aggregate-persistence-write-back.md @@ -0,0 +1,91 @@ +# ADR 0016: Aggregate Persistence Write-Back on Commit + +- Status: Accepted +- Date: 2026-06-12 + +## Context + +[ADR 0011](0011-pydantic-message-schemas-with-plain-domain-aggregates.md) +keeps domain aggregates as plain Python objects and isolates them from +SQLAlchemy. The repository therefore uses the *translation* pattern: it maps +between the `User` aggregate and the `UserRecord` SQLAlchemy model on the way +in and out of persistence. + +That isolation has a sharp edge the book's classic mapper-based repository does +not have. The book maps the domain object onto the ORM, so the SQLAlchemy +identity map and unit of work track every mutation and flush it on commit. With +translation, a domain aggregate loaded via `repository.get(...)` is a fresh, +detached object. SQLAlchemy never sees it. Mutating that aggregate and calling +`uow.commit()` previously persisted **nothing** for loaded-then-mutated +aggregates: the only writes that reached the database were the explicit +`session.add(...)` calls made by `repository.add(...)` for brand-new +aggregates. Updates were silently lost. + +The repository also tracked seen aggregates in a list, which appended +duplicates when the same row was loaded twice and offered no way to return the +same instance for the same identity within a transaction. + +## Decision + +Restore write-on-commit for the translation repository without coupling the +domain to SQLAlchemy. + +- Track seen aggregates in an **identity map** keyed by aggregate id + (`dict[UUID, User]`) instead of a list. `get(...)` returns the + already-tracked instance for an identity, so callers mutate one shared + aggregate per transaction and re-loading a row does not create duplicates. +- On `commit()`, the unit of work asks the repository to persist tracked + changes. The repository writes each tracked aggregate back with + `session.merge(self._to_record(user))`. `merge` upserts by primary key, + which unifies inserts (new aggregates also added via `session.add`) and + updates (aggregates loaded then mutated). The session then commits. +- Domain aggregates stay plain Python. The write-back lives entirely in the + persistence adapter and the SQLAlchemy unit of work. + +[ADR 0011](0011-pydantic-message-schemas-with-plain-domain-aggregates.md) +remains Accepted. This ADR extends its persistence implications; it does not +supersede it. + +## Consequences + +Mutations to loaded aggregates now persist on commit, matching the mental model +developers expect from the book and from ORM-backed repositories. The identity +map fixes duplicate tracking and guarantees one instance per identity per +transaction, which also keeps domain-event collection correct. + +The cost is an explicit `merge` per tracked aggregate at commit time. `merge` +issues a primary-key lookup, so very large transactions touching many +aggregates pay for that lookup per aggregate. This is acceptable because +write commands target a single aggregate root by default +([ADR 0013](0013-aggregates-define-consistency-boundaries.md)). Unique-constraint +violations still surface at flush, so the check-then-commit duplicate-email +handling and the `IntegrityConflict` to `EmailAlreadyRegistered` translation in +the handlers continue to work unchanged. + +Query-only paths must keep using purpose-built read models +([ADR 0014](0014-cqrs-read-models-are-purpose-built.md)) rather than loading +write-side aggregates, so this write-back cost only applies to genuine command +handling. + +## Agent Guidance + +- Load an aggregate through `uow.users.get(...)`, mutate it with domain + behavior, and call `uow.commit()`. Do not reach into the SQLAlchemy session + to update rows by hand. +- Keep aggregate state translation inside the repository + (`_to_record` / `_to_domain`). Do not leak SQLAlchemy records into handlers + or the domain. +- Keep write commands focused on one aggregate root by default. +- For query paths, use a reader port and read model instead of loading an + aggregate just to read it. +- When adding a new aggregate type, give its repository an identity map and a + `persist_changes`-style write-back, and call it from the unit of work's + `commit`. + +## References + +- [ADR 0011: Pydantic Message Schemas With Plain Domain Aggregates](0011-pydantic-message-schemas-with-plain-domain-aggregates.md) +- [ADR 0013: Aggregates Define Consistency Boundaries](0013-aggregates-define-consistency-boundaries.md) +- [ADR 0014: CQRS Read Models Are Purpose Built](0014-cqrs-read-models-are-purpose-built.md) +- [Cosmic Python: Unit of Work](https://www.cosmicpython.com/book/chapter_06_uow.html) +- [SQLAlchemy: Session.merge](https://docs.sqlalchemy.org/en/20/orm/session_state_management.html#merging) diff --git a/docs/adr/README.md b/docs/adr/README.md index f78d0c1..03ac73e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -33,6 +33,7 @@ treated as migration work, not as a precedent to repeat. | [0012](0012-camel-case-json-message-contracts.md) | Camel-Case JSON Message Contracts | Accepted | | [0013](0013-aggregates-define-consistency-boundaries.md) | Aggregates Define Consistency Boundaries | Accepted | | [0014](0014-cqrs-read-models-are-purpose-built.md) | CQRS Read Models Are Purpose Built | Accepted | +| [0016](0016-aggregate-persistence-write-back.md) | Aggregate Persistence Write-Back on Commit | Accepted | ## Agent Checklist diff --git a/pyproject.toml b/pyproject.toml index c4857c1..07c647f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "1.0.0" description = "" authors = [{ name = "Tomas Sanchez", email = "info@tomsanchez.com.ar" }] readme = "README.md" -requires-python = ">=3.10,<4.0" +requires-python = ">=3.13" dependencies = [ "fastapi>=0.115.0", "uvicorn>=0.34.0", @@ -21,7 +21,7 @@ dev = [ "pre-commit>=4.2.0", "httpx>=0.28.0", "pytest-cov>=6.2.0", - "ruff", + "ruff>=0.12,<0.13", "pyrefly>=1.0.0", ] @@ -31,7 +31,6 @@ build-backend = "hatchling.build" [tool.ruff] line-length = 120 -target-version = "py311" [tool.ruff.lint] select = [ @@ -66,11 +65,8 @@ select = [ # tryceratops "TRY", ] -# Some pydocstyle rules are disabled because they are often too verbose. +# Disable rules that are too noisy for this scaffold. ignore = [ - "D100", - "D104", - "D107", # LineTooLong "E501", # DoNotAssignLambda @@ -85,7 +81,7 @@ known-first-party = ["template"] [tool.coverage.run] branch = true -omit = ['tests/*', 'src/template/asgi.py', 'src/**/__init__.py'] +omit = ['tests/*', 'src/template/asgi.py', 'src/template/main.py', 'src/**/__init__.py'] [tool.coverage.report] show_missing = true diff --git a/src/template/adapters/models/base.py b/src/template/adapters/models/base.py index 239b76a..8a7b289 100644 --- a/src/template/adapters/models/base.py +++ b/src/template/adapters/models/base.py @@ -1,88 +1,7 @@ """Shared SQLAlchemy persistence model helpers.""" -from typing import Any - -from pydantic import BaseModel, ValidationError -from sqlalchemy import JSON, Dialect, TypeDecorator -from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): """Provide the declarative base for persistence records.""" - - def model_dump(self) -> dict[str, Any]: - """Return mapped column values as a dictionary.""" - result = {} - for column in self.__table__.columns: # type: ignore[attr-defined] - result[column.name] = self._dump_value(getattr(self, column.name)) - return result - - def _dump_value(self, value: Any) -> Any: - """Recursively dump nested Pydantic values.""" - if value is None: - return None - if isinstance(value, BaseModel): - return value.model_dump(exclude_none=True) - if isinstance(value, list): - return [self._dump_value(item) for item in value] - return value - - -class BasePydanticField(TypeDecorator): - """Store Pydantic models in a JSON column.""" - - impl = JSON().with_variant(JSONB(), "postgresql") - cache_ok = True - - def __init__(self, model_class: type[BaseModel], *args, **kwargs): - """Initialize the field for one Pydantic model class.""" - if not issubclass(model_class, BaseModel): - raise TypeError - super().__init__(*args, **kwargs) - self.model_class = model_class - - def _validate_and_dump_model(self, model: BaseModel) -> dict[str, Any]: - """Validate and serialize one Pydantic model.""" - if not isinstance(model, self.model_class): - raise TypeError - return model.model_dump(exclude_none=True) - - def _validate_and_load_model(self, data: dict[str, Any]) -> BaseModel: - """Validate and deserialize one Pydantic model.""" - try: - return self.model_class.model_validate(data) - except ValidationError as error: - raise ValueError(error) from error - - -class PydanticModelField(BasePydanticField): - """Store one Pydantic model in a JSON column.""" - - def process_bind_param(self, value: BaseModel | None, dialect: Dialect) -> dict[str, Any] | None: - """Serialize a value before persistence.""" - return None if value is None else self._validate_and_dump_model(value) - - def process_result_value(self, value: dict[str, Any] | None, dialect: Dialect) -> BaseModel | None: - """Deserialize a value loaded from persistence.""" - return None if value is None else self._validate_and_load_model(value) - - -class PydanticModelListField(BasePydanticField): - """Store a list of Pydantic models in a JSON column.""" - - def process_bind_param(self, value: list[BaseModel] | None, dialect: Dialect) -> list[dict[str, Any]] | None: - """Serialize values before persistence.""" - if value is None: - return None - if not isinstance(value, list): - raise TypeError - return [self._validate_and_dump_model(item) for item in value] - - def process_result_value(self, value: list[dict[str, Any]] | None, dialect: Dialect) -> list[BaseModel] | None: - """Deserialize values loaded from persistence.""" - if value is None: - return None - if not isinstance(value, list): - raise TypeError - return [self._validate_and_load_model(item) for item in value] diff --git a/src/template/adapters/repository.py b/src/template/adapters/repository.py index ab40619..05dd81d 100644 --- a/src/template/adapters/repository.py +++ b/src/template/adapters/repository.py @@ -22,15 +22,21 @@ def __init__(self, session: Session): session: SQLAlchemy session owned by the unit of work. """ self.session = session - self.seen: list[User] = [] + self.seen: dict[UUID, User] = {} def add(self, user: User) -> None: - """Persist a new user.""" + """Persist a new user and track it in the identity map.""" self.session.add(self._to_record(user)) - self.seen.append(user) + self.seen[user.id] = user def get(self, user_id: UUID) -> User | None: - """Return a user by identity.""" + """Return a user by identity. + + Returns the already-tracked instance when the row was loaded earlier + in this transaction so callers always mutate the same aggregate. + """ + if user_id in self.seen: + return self.seen[user_id] return self._remember(self.session.get(UserRecord, str(user_id))) def get_by_email(self, email: str) -> User | None: @@ -39,13 +45,27 @@ def get_by_email(self, email: str) -> User | None: return self._remember(record) def _remember(self, record: UserRecord | None) -> User | None: - """Translate and track a loaded aggregate.""" + """Translate and track a loaded aggregate in the identity map.""" if record is None: return None + if UUID(record.id) in self.seen: + return self.seen[UUID(record.id)] user = self._to_domain(record) - self.seen.append(user) + self.seen[user.id] = user return user + def persist_changes(self) -> None: + """Write the current state of tracked aggregates back to the session. + + The translation pattern detaches domain aggregates from SQLAlchemy + change tracking, so mutations made after loading do not flush + automatically. Merging each tracked record upserts by primary key, + unifying inserts (new aggregates added in this transaction) and + updates (aggregates loaded then mutated). + """ + for user in self.seen.values(): + self.session.merge(self._to_record(user)) + @staticmethod def _to_record(user: User) -> UserRecord: """Translate a domain aggregate into a persistence record.""" diff --git a/src/template/adapters/unit_of_work.py b/src/template/adapters/unit_of_work.py index 970aec1..b42f5cd 100644 --- a/src/template/adapters/unit_of_work.py +++ b/src/template/adapters/unit_of_work.py @@ -39,8 +39,9 @@ def __exit__( self.session.close() def commit(self) -> None: - """Commit the SQLAlchemy transaction.""" + """Persist tracked aggregate changes and commit the transaction.""" try: + self.users.persist_changes() self.session.commit() except IntegrityError as error: raise IntegrityConflict from error diff --git a/src/template/asgi.py b/src/template/asgi.py index d747060..28a1557 100644 --- a/src/template/asgi.py +++ b/src/template/asgi.py @@ -79,10 +79,16 @@ def get_application(container: ApplicationContainer | None = None) -> FastAPI: ) app.state.container = container or bootstrap() + # A wildcard origin reflected together with credentials is the most + # permissive (and dangerous) CORS posture: browsers will not honor it, and + # it effectively trusts any site. Only enable credentials when origins are + # an explicit allow-list rather than the "*" wildcard. + allow_credentials = "*" not in settings.BACKEND_CORS_ORIGINS + app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, + allow_origins=settings.BACKEND_CORS_ORIGINS, + allow_credentials=allow_credentials, allow_methods=["*"], allow_headers=["*"], ) diff --git a/src/template/bootstrap.py b/src/template/bootstrap.py index eac5336..9a153d9 100644 --- a/src/template/bootstrap.py +++ b/src/template/bootstrap.py @@ -7,14 +7,15 @@ from functools import partial from sqlalchemy import Engine, create_engine +from sqlalchemy.engine import make_url from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool from template.adapters.models.base import Base from template.adapters.queries import SqlAlchemyUserReader from template.adapters.unit_of_work import SqlAlchemyUnitOfWork -from template.domain.commands.user import RegisterUser -from template.domain.events.user import UserRegistered +from template.domain.commands.user import DeactivateUser, RegisterUser +from template.domain.events.user import UserDeactivated, UserRegistered from template.service_layer import handlers from template.service_layer.messagebus import MessageBus from template.service_layer.queries import UserReader @@ -26,6 +27,10 @@ def _ignore_user_registered(event: UserRegistered) -> None: """Provide a default no-op external event publisher.""" +def _ignore_user_deactivated(event: UserDeactivated) -> None: + """Provide a default no-op external event publisher.""" + + @dataclass class ApplicationContainer: """Hold process-level application dependencies.""" @@ -62,7 +67,11 @@ def bootstrap( """ settings = database_settings or DatabaseSettings() engine_options = {} - if settings.URL == "sqlite+pysqlite://": + url = make_url(settings.URL) + if url.get_backend_name() == "sqlite" and url.database in (None, "", ":memory:"): + # An in-memory SQLite database lives inside one connection. Share a + # single static-pooled connection so the schema and data created at + # startup remain visible to every unit of work. engine_options = {"connect_args": {"check_same_thread": False}, "poolclass": StaticPool} engine = create_engine(settings.URL, **engine_options) session_factory = sessionmaker(bind=engine, expire_on_commit=False) @@ -70,8 +79,14 @@ def bootstrap( user_reader = SqlAlchemyUserReader(session_factory) bus = MessageBus( uow_factory=uow_factory, - command_handlers={RegisterUser: handlers.register_user}, - event_handlers={UserRegistered: [partial(handlers.publish_user_registered, publish=publish)]}, + command_handlers={ + RegisterUser: handlers.register_user, + DeactivateUser: handlers.deactivate_user, + }, + event_handlers={ + UserRegistered: [partial(handlers.publish_user_registered, publish=publish)], + UserDeactivated: [partial(handlers.publish_user_deactivated, publish=_ignore_user_deactivated)], + }, ) return ApplicationContainer( engine=engine, diff --git a/src/template/domain/commands/user.py b/src/template/domain/commands/user.py index 1435a56..a39647f 100644 --- a/src/template/domain/commands/user.py +++ b/src/template/domain/commands/user.py @@ -34,3 +34,9 @@ class RegisterUser(Command): email: EmailStr settings: RegisterUserSettings = Field(default_factory=RegisterUserSettings) user_id: UUID = Field(default_factory=uuid4) + + +class DeactivateUser(Command): + """Request deactivation of one user.""" + + user_id: UUID diff --git a/src/template/domain/events/user.py b/src/template/domain/events/user.py index c33d1db..278aff1 100644 --- a/src/template/domain/events/user.py +++ b/src/template/domain/events/user.py @@ -10,3 +10,9 @@ class UserRegistered(Event): user_id: UUID email: str + + +class UserDeactivated(Event): + """Record that a user was deactivated.""" + + user_id: UUID diff --git a/src/template/domain/models/base.py b/src/template/domain/models/base.py deleted file mode 100644 index f0275fb..0000000 --- a/src/template/domain/models/base.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Shared domain model building blocks.""" - -from enum import Enum - - -class BaseEnum(str, Enum): - """Base class for string enumerations used in the domain.""" - - @classmethod - def list(cls) -> list[str]: - """Return all enumeration values.""" - return [member.value for member in cls] diff --git a/src/template/domain/models/user.py b/src/template/domain/models/user.py index 54c0375..38760c3 100644 --- a/src/template/domain/models/user.py +++ b/src/template/domain/models/user.py @@ -7,7 +7,8 @@ from typing import Literal from uuid import UUID, uuid4 -from template.domain.events.user import UserRegistered +from template.domain.events.user import UserDeactivated, UserRegistered +from template.domain.messages import Event @dataclass(frozen=True) @@ -30,7 +31,7 @@ class User: id: UUID = field(default_factory=uuid4) is_active: bool = True created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) - events: list[UserRegistered] = field(default_factory=list, compare=False) + events: list[Event] = field(default_factory=list, compare=False) @classmethod def register( @@ -59,3 +60,14 @@ def register( ) user.events.append(UserRegistered(user_id=user.id, email=user.email)) return user + + def deactivate(self) -> None: + """Deactivate the user and record the resulting domain event. + + Deactivation is idempotent: deactivating an already-inactive user + leaves the aggregate unchanged and records no further event. + """ + if not self.is_active: + return + self.is_active = False + self.events.append(UserDeactivated(user_id=self.id)) diff --git a/src/template/entrypoint/dependencies.py b/src/template/entrypoint/dependencies.py new file mode 100644 index 0000000..46a1d24 --- /dev/null +++ b/src/template/entrypoint/dependencies.py @@ -0,0 +1,20 @@ +"""Shared FastAPI dependencies for entrypoints. + +Resolves the process-level application container from request state so that +entrypoint handlers depend on the composition root through dependency +injection rather than module-level globals. +""" + +from typing import Annotated + +from fastapi import Depends, Request + +from template.bootstrap import ApplicationContainer + + +def get_container(request: Request) -> ApplicationContainer: + """Return application dependencies from FastAPI state.""" + return request.app.state.container + + +Container = Annotated[ApplicationContainer, Depends(get_container)] diff --git a/src/template/entrypoint/monitor.py b/src/template/entrypoint/monitor.py index 519128b..402f1ad 100644 --- a/src/template/entrypoint/monitor.py +++ b/src/template/entrypoint/monitor.py @@ -3,11 +3,18 @@ Responsible for probing the system liveness and readiness. """ +import logging + from fastapi import APIRouter, status -from starlette.responses import RedirectResponse +from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError +from starlette.responses import JSONResponse, RedirectResponse +from template.entrypoint.dependencies import Container from template.entrypoint.schemas import LivenessProbed, ReadinessProbed, ResponseModel +log = logging.getLogger(__name__) + router = APIRouter() @@ -32,14 +39,31 @@ async def query_liveness_probe() -> ResponseModel[LivenessProbed]: tags=["Monitor"], name="Readiness", status_code=status.HTTP_200_OK, + response_model=ResponseModel[ReadinessProbed], ) -async def query_readiness_probe() -> ResponseModel[ReadinessProbed]: +def query_readiness_probe(container: Container) -> ResponseModel[ReadinessProbed] | JSONResponse: """ Probe the system readiness. - When working with Kubernetes, Checks if the pod is ready to handle incoming traffic and requests. If the - readiness probe fails, Kubernetes temporarily stops sending traffic to the pod. + When working with Kubernetes, checks if the pod is ready to handle incoming traffic and requests. The probe + verifies that the database is reachable by running a lightweight ``SELECT 1`` against the configured engine. + If the readiness probe fails, Kubernetes temporarily stops sending traffic to the pod. + + Returns: + A ``Ready`` response with HTTP 200 when the database responds, otherwise an ``Error`` response with + HTTP 503 so orchestrators stop routing traffic to this pod. """ + try: + with container.engine.connect() as connection: + connection.execute(text("SELECT 1")) + except SQLAlchemyError: + log.warning("Readiness probe failed: database is unreachable.", exc_info=True) + body = ResponseModel(data=ReadinessProbed(status="Error")) + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content=body.model_dump(mode="json", by_alias=True), + ) + return ResponseModel(data=ReadinessProbed()) diff --git a/src/template/entrypoint/schemas.py b/src/template/entrypoint/schemas.py index 307c2e2..067fe3a 100644 --- a/src/template/entrypoint/schemas.py +++ b/src/template/entrypoint/schemas.py @@ -1,6 +1,6 @@ """Pydantic schemas used at application boundaries.""" -from typing import Generic, Literal, TypeVar +from typing import Literal from pydantic import BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel @@ -18,10 +18,7 @@ class CamelCaseModel(BaseModel): ) -S = TypeVar("S", bound=CamelCaseModel) - - -class ResponseModel(CamelCaseModel, Generic[S]): +class ResponseModel[S: CamelCaseModel](CamelCaseModel): """Wrap successful API response data.""" data: S | list[S] = Field(description="The response data.") diff --git a/src/template/entrypoint/users.py b/src/template/entrypoint/users.py index a0cac58..1eb3b47 100644 --- a/src/template/entrypoint/users.py +++ b/src/template/entrypoint/users.py @@ -1,13 +1,13 @@ """FastAPI entrypoints for user use cases.""" -from typing import Annotated, Literal +from typing import Literal from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi import APIRouter, HTTPException, status from pydantic import EmailStr, Field -from template.bootstrap import ApplicationContainer from template.domain.commands.user import RegisterUser +from template.entrypoint.dependencies import Container from template.entrypoint.schemas import CamelCaseModel, ResponseModel from template.service_layer.handlers import EmailAlreadyRegistered from template.service_layer.queries import get_user @@ -63,14 +63,6 @@ def from_read_model(cls, user: UserReadModel) -> "UserResponse": ) -def get_container(request: Request) -> ApplicationContainer: - """Return application dependencies from FastAPI state.""" - return request.app.state.container - - -Container = Annotated[ApplicationContainer, Depends(get_container)] - - @router.post("", status_code=status.HTTP_201_CREATED) def register_user(payload: RegisterUserRequest, container: Container) -> ResponseModel[UserResponse]: """Register a user.""" diff --git a/src/template/main.py b/src/template/main.py index 63f88e1..f7633fc 100644 --- a/src/template/main.py +++ b/src/template/main.py @@ -10,10 +10,8 @@ if __name__ == "__main__": - # pylint: disable=wrong-import-position import uvicorn - # pylint: disable=ungrouped-imports from template.settings.uvicorn_settings import UvicornSettings settings = UvicornSettings() diff --git a/src/template/service_layer/handlers.py b/src/template/service_layer/handlers.py index 585ac67..fba04c9 100644 --- a/src/template/service_layer/handlers.py +++ b/src/template/service_layer/handlers.py @@ -3,8 +3,8 @@ from collections.abc import Callable from uuid import UUID -from template.domain.commands.user import RegisterUser -from template.domain.events.user import UserRegistered +from template.domain.commands.user import DeactivateUser, RegisterUser +from template.domain.events.user import UserDeactivated, UserRegistered from template.domain.models.user import User from template.service_layer.unit_of_work import AbstractUnitOfWork, IntegrityConflict @@ -13,6 +13,15 @@ class EmailAlreadyRegistered(ValueError): """Raised when a registration uses an existing email address.""" +class UserNotFound(LookupError): + """Raised when a command targets an unknown user identity.""" + + def __init__(self, user_id: UUID): + """Initialize the error for a missing user.""" + self.user_id = user_id + super().__init__(f"User {user_id} not found") + + def register_user(command: RegisterUser, uow: AbstractUnitOfWork) -> UUID: """Register a user. @@ -43,6 +52,38 @@ def register_user(command: RegisterUser, uow: AbstractUnitOfWork) -> UUID: return user.id +def deactivate_user(command: DeactivateUser, uow: AbstractUnitOfWork) -> UUID: + """Deactivate a user. + + Loads the aggregate through the unit of work, applies the deactivation + behavior, and commits. The write-back on commit persists the mutated + aggregate state even though the translation repository detaches it from + SQLAlchemy change tracking. + + Args: + command: User deactivation request. + uow: Transaction boundary. + + Returns: + The deactivated user identity. + + Raises: + UserNotFound: If no user exists for the requested identity. + """ + with uow: + user = uow.users.get(command.user_id) + if user is None: + raise UserNotFound(command.user_id) + user.deactivate() + uow.commit() + return command.user_id + + def publish_user_registered(event: UserRegistered, publish: Callable[[UserRegistered], None]) -> None: """Publish user registration for interested external adapters.""" publish(event) + + +def publish_user_deactivated(event: UserDeactivated, publish: Callable[[UserDeactivated], None]) -> None: + """Publish user deactivation for interested external adapters.""" + publish(event) diff --git a/src/template/service_layer/messagebus.py b/src/template/service_layer/messagebus.py index 93374ce..3724c8c 100644 --- a/src/template/service_layer/messagebus.py +++ b/src/template/service_layer/messagebus.py @@ -24,6 +24,14 @@ def __init__(self, message: Message): super().__init__(f"Unsupported message type: {type(message).__name__}") +class UnhandledCommand(KeyError): + """Raised when no handler is registered for a dispatched command.""" + + def __init__(self, command: Command): + """Initialize the error for a command without a registered handler.""" + super().__init__(f"No handler registered for command: {type(command).__name__}") + + class MessageBus: """Dispatch commands and events to configured handlers.""" @@ -54,8 +62,11 @@ def handle(self, message: Message) -> Any: def _handle_command(self, command: Command, queue: list[Message]) -> Any: """Dispatch a command and collect resulting events.""" + try: + handler = self.command_handlers[type(command)] + except KeyError as error: + raise UnhandledCommand(command) from error uow = self.uow_factory() - handler = self.command_handlers[type(command)] result = handler(command, uow) queue.extend(uow.collect_new_events()) return result diff --git a/src/template/service_layer/repository.py b/src/template/service_layer/repository.py index dc68c91..221b658 100644 --- a/src/template/service_layer/repository.py +++ b/src/template/service_layer/repository.py @@ -9,7 +9,7 @@ class UserRepository(Protocol): """Describe persistence operations required by user handlers.""" - seen: list[User] + seen: dict[UUID, User] def add(self, user: User) -> None: """Persist a new user.""" @@ -19,3 +19,6 @@ def get(self, user_id: UUID) -> User | None: def get_by_email(self, email: str) -> User | None: """Return a user by normalized email address.""" + + def persist_changes(self) -> None: + """Write tracked aggregate state back to the underlying session.""" diff --git a/src/template/service_layer/unit_of_work.py b/src/template/service_layer/unit_of_work.py index 7324969..f912bfd 100644 --- a/src/template/service_layer/unit_of_work.py +++ b/src/template/service_layer/unit_of_work.py @@ -41,7 +41,14 @@ def rollback(self) -> None: """Roll back the current transaction.""" def collect_new_events(self) -> Iterator[Event]: - """Yield pending events from aggregates seen in this transaction.""" - for user in self.users.seen: + """Yield pending events from aggregates seen in this transaction. + + A unit of work that was never entered has no repository; in that case + no aggregate was seen and the iterator yields nothing. + """ + users = getattr(self, "users", None) + if users is None: + return + for user in users.seen.values(): while user.events: yield user.events.pop(0) diff --git a/src/template/settings/api_settings.py b/src/template/settings/api_settings.py index f10adff..154b0fb 100644 --- a/src/template/settings/api_settings.py +++ b/src/template/settings/api_settings.py @@ -49,6 +49,7 @@ class ApplicationSettings(BaseSettings): * FASTAPI_PROJECT_CONTACT * FASTAPI_VERSION * FASTAPI_DOCS_URL + * FASTAPI_BACKEND_CORS_ORIGINS Attributes: DEBUG (bool): FastAPI logging level. You should disable this for @@ -59,20 +60,23 @@ class ApplicationSettings(BaseSettings): PROJECT_CONTACT (ContactInfo): FastAPI project contact details. VERSION (str): Application version. DOCS_URL (str): Path where swagger ui will be served at. + BACKEND_CORS_ORIGINS (list[str]): Origins allowed by the CORS + middleware. Defaults to a safe localhost allow-list. Resources: 1. https://docs.pydantic.dev/latest/usage/pydantic_settings/ """ - DEBUG: bool = True - PROJECT_NAME: str = "Cosmic FastAPI Template" + DEBUG: bool = False + PROJECT_NAME: str = "Cosmic FastAPI" PROJECT_DESCRIPTION: str = "This is a FastAPI template demo." PROJECT_LICENSE: LicenseInfo | None = LicenseInfo(name="MIT", url="https://mit-license.org/") PROJECT_CONTACT: ContactInfo | None = ContactInfo( - name="Tom Sanchez", url="https://tomsanchez.com.ar", email="info@tomsanchez.com.ar" + name="Tomas Sanchez", url="https://tomsanchez.com.ar", email="info@tomsanchez.com.ar" ) VERSION: str = __version__ DOCS_URL: str = "/docs" + BACKEND_CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:8000"] # All your additional application configuration should go either here or in # separate file in this submodule. diff --git a/src/template/utils/__init__.py b/src/template/utils/__init__.py deleted file mode 100644 index c997a1b..0000000 --- a/src/template/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Utils - -This package contains utility functions and classes that are shared across all layers. -""" diff --git a/src/template/version.py b/src/template/version.py index d116fa3..3fac7c6 100644 --- a/src/template/version.py +++ b/src/template/version.py @@ -2,4 +2,11 @@ Application Version """ -__version__: str = "1.0.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__: str = version("template") +except PackageNotFoundError: # pragma: no cover + # Fallback when the package metadata is unavailable (e.g. running from a + # source tree without an installed distribution). + __version__ = "0.0.0" diff --git a/tests/conftest.py b/tests/conftest.py index c073c5c..6ea25dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,18 +2,29 @@ Pytest Fixtures. """ +from collections.abc import Iterator + import pytest from starlette.testclient import TestClient -from template.main import app +from template.asgi import get_application +from template.bootstrap import bootstrap +from template.settings.database_settings import DatabaseSettings @pytest.fixture(name="test_client") -def fixture_test_client() -> TestClient: +def fixture_test_client() -> Iterator[TestClient]: """ - Create a test client for the FastAPI application. + Create a test client backed by an isolated in-memory application. + + Builds the production app from an injected container that uses an in-memory + SQLite database, so collection never touches a real ``DATABASE_URL`` or + writes a file to the repository. The client runs inside a ``with`` block so + the FastAPI lifespan executes and disposes resources on teardown. - Returns: + Yields: TestClient: A test client for the app. """ - return TestClient(app) + container = bootstrap(DatabaseSettings(URL="sqlite+pysqlite://", AUTO_CREATE_SCHEMA=True)) + with TestClient(get_application(container)) as client: + yield client diff --git a/tests/e2e/entrypoint/test_monitor.py b/tests/e2e/entrypoint/test_monitor.py index 4c17cfe..6f02535 100644 --- a/tests/e2e/entrypoint/test_monitor.py +++ b/tests/e2e/entrypoint/test_monitor.py @@ -4,8 +4,13 @@ import pytest from fastapi import status +from sqlalchemy.exc import SQLAlchemyError +from starlette.testclient import TestClient +from template.asgi import get_application +from template.bootstrap import bootstrap from template.entrypoint.schemas import LivenessProbed, ReadinessProbed, ResponseModel +from template.settings.database_settings import DatabaseSettings class TestMonitorEntryPoint: @@ -40,11 +45,11 @@ def test_liveness_probe(self, test_client): except ValueError: pytest.fail("Response body is not a valid LivenessProbed JSON") - def test_readiness_probe(self, test_client): + def test_readiness_probe_when_database_is_reachable(self, test_client): """ - GIVEN a FastAPI application configured with the Monitor Entrypoint + GIVEN a FastAPI application whose database is reachable WHEN the readiness probe is requested "GET /readiness" - THEN it should return 200, and a valid ReadinessProbed JSON + THEN it should return 200, and a ReadinessProbed JSON with status "Ready" """ # when @@ -53,6 +58,38 @@ def test_readiness_probe(self, test_client): # then assert response.status_code == status.HTTP_200_OK try: - ResponseModel[ReadinessProbed].model_validate_json(response.content) + probe = ResponseModel[ReadinessProbed].model_validate_json(response.content) + except ValueError: + pytest.fail("Response body is not a valid ReadinessProbed JSON") + assert isinstance(probe.data, ReadinessProbed) + assert probe.data.status == "Ready" + + def test_readiness_probe_when_database_is_unreachable(self): + """ + GIVEN a FastAPI application whose database engine has been disposed + WHEN the readiness probe is requested "GET /readiness" + THEN it should return 503, and a ReadinessProbed JSON with status "Error" + """ + + # given + container = bootstrap(DatabaseSettings(URL="sqlite+pysqlite://", AUTO_CREATE_SCHEMA=True)) + app = get_application(container) + with TestClient(app) as client: + # Force every subsequent connection attempt to fail so the probe + # exercises its error branch even with an in-memory StaticPool. + def _raise_on_connect() -> None: + raise SQLAlchemyError + + container.engine.connect = _raise_on_connect # type: ignore[method-assign] + + # when + response = client.get("/readiness") + + # then + assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + try: + probe = ResponseModel[ReadinessProbed].model_validate_json(response.content) except ValueError: pytest.fail("Response body is not a valid ReadinessProbed JSON") + assert isinstance(probe.data, ReadinessProbed) + assert probe.data.status == "Error" diff --git a/tests/integration/test_persistence.py b/tests/integration/test_persistence.py index afd38d3..26d2298 100644 --- a/tests/integration/test_persistence.py +++ b/tests/integration/test_persistence.py @@ -119,6 +119,55 @@ def test_declares_timezone_aware_timestamp_storage(self): assert isinstance(column_type, DateTime) assert column_type.timezone is True + def test_persists_mutations_to_loaded_aggregates(self, session_factory: sessionmaker[Session]): + """ + GIVEN a persisted user loaded through a fresh unit of work + WHEN the loaded aggregate is mutated and committed + THEN a later unit of work observes the mutated state + + This guards against the silent data loss that the translation pattern + causes: a loaded aggregate is detached from SQLAlchemy change tracking, + so its mutations must be written back explicitly on commit. + """ + # GIVEN + user = User.register(name="Ada Lovelace", email="ada@example.com") + with SqlAlchemyUnitOfWork(session_factory) as uow: + uow.users.add(user) + uow.commit() + + # WHEN + with SqlAlchemyUnitOfWork(session_factory) as uow: + loaded_user = uow.users.get(user.id) + assert loaded_user is not None + loaded_user.deactivate() + uow.commit() + + # THEN + with SqlAlchemyUnitOfWork(session_factory) as uow: + reloaded_user = uow.users.get(user.id) + assert reloaded_user is not None + assert reloaded_user.is_active is False + + def test_returns_the_same_instance_for_a_re_loaded_aggregate(self, session_factory: sessionmaker[Session]): + """ + GIVEN a persisted user + WHEN the same identity is loaded twice within one unit of work + THEN the repository returns the identical tracked instance + """ + # GIVEN + user = User.register(name="Ada Lovelace", email="ada@example.com") + with SqlAlchemyUnitOfWork(session_factory) as uow: + uow.users.add(user) + uow.commit() + + # WHEN / THEN + with SqlAlchemyUnitOfWork(session_factory) as uow: + first = uow.users.get(user.id) + by_email = uow.users.get_by_email("ada@example.com") + second = uow.users.get(user.id) + assert first is second + assert by_email is first + def test_translates_integrity_errors(self, session_factory: sessionmaker[Session]): """ GIVEN two users with the same globally unique email diff --git a/tests/unit/domain/models/test_base.py b/tests/unit/domain/models/test_base.py deleted file mode 100644 index 1a2539c..0000000 --- a/tests/unit/domain/models/test_base.py +++ /dev/null @@ -1,132 +0,0 @@ -"""tests/unit/domain/models/test_base.py -Unit test cases for BaseModel -""" - -from typing import Any, cast - -import pytest -from pydantic import BaseModel -from sqlalchemy.orm import Mapped, mapped_column - -from template.adapters.models.base import Base, BasePydanticField, PydanticModelField, PydanticModelListField -from template.domain.models.base import BaseEnum - - -class TestBase: - def test_dumps_to_dict(self): - """ - GIVEN a Base class with columns - AND an instance of the class - WHEN model_dump is called - THEN it maps to dict - """ - - # GIVEN - class TestModel(Base): - __tablename__ = "test_model" - test_id: Mapped[int] = mapped_column(primary_key=True) - foo: Mapped[str] - bar: Mapped[int] - foobar: Mapped[bool] - - instance = TestModel(test_id=1, foo="foo", bar=1, foobar=True) - - # WHEN - result = instance.model_dump() - - # THEN - assert result == {"test_id": 1, "foo": "foo", "bar": 1, "foobar": True} - - def test_dumps_recursively(self): - """ - GIVEN a pydantic model field - AND a Base class with nested PydanticModel fields - AND an instance of the class - WHEN model_dump is called - THEN it maps to dict with pydantic dicts - """ - - class TestPydanticField(BaseModel): - foo: str = "foo" - bar: int = 0 - - # AND - class NestedTestModel(Base): - __tablename__ = "test_pydantic_model" - test_id: Mapped[int] = mapped_column(primary_key=True) - foobar: Mapped[TestPydanticField] = mapped_column(PydanticModelField(TestPydanticField)) - foobars: Mapped[list[TestPydanticField]] = mapped_column(PydanticModelListField(TestPydanticField)) - nullable: Mapped[str | None] - - # AND - instance = NestedTestModel(test_id=1, foobar=TestPydanticField(), foobars=[TestPydanticField()], nullable=None) - - # WHEN - result = instance.model_dump() - - # THEN - assert result == { - "test_id": 1, - "foobar": {"foo": "foo", "bar": 0}, - "foobars": [{"foo": "foo", "bar": 0}], - "nullable": None, - } - - def test_validates_pydantic_json_fields(self): - """ - GIVEN SQLAlchemy Pydantic JSON field adapters - WHEN values are bound and loaded - THEN models are serialized, restored, and invalid values are rejected - """ - - # GIVEN - class TestPydanticField(BaseModel): - foo: str - - class OtherPydanticField(BaseModel): - bar: str - - single_field = PydanticModelField(TestPydanticField) - list_field = PydanticModelListField(TestPydanticField) - model = TestPydanticField(foo="foo") - - # WHEN / THEN - assert single_field.process_bind_param(None, None) is None # type: ignore[arg-type] - assert single_field.process_bind_param(model, None) == {"foo": "foo"} # type: ignore[arg-type] - assert single_field.process_result_value(None, None) is None # type: ignore[arg-type] - assert single_field.process_result_value({"foo": "foo"}, None) == model # type: ignore[arg-type] - assert list_field.process_bind_param(None, None) is None # type: ignore[arg-type] - assert list_field.process_bind_param([model], None) == [{"foo": "foo"}] # type: ignore[arg-type] - assert list_field.process_result_value(None, None) is None # type: ignore[arg-type] - assert list_field.process_result_value([{"foo": "foo"}], None) == [model] # type: ignore[arg-type] - with pytest.raises(TypeError): - BasePydanticField(cast(Any, str)) - with pytest.raises(TypeError): - single_field._validate_and_dump_model(OtherPydanticField(bar="bar")) - with pytest.raises(TypeError): - list_field.process_bind_param(model, None) # type: ignore[arg-type] - with pytest.raises(TypeError): - list_field.process_result_value({"foo": "foo"}, None) # type: ignore[arg-type] - with pytest.raises(ValueError): - single_field.process_result_value({}, None) # type: ignore[arg-type] - - -class TestBaseEnum: - def test_list_values(self): - """ - GIVEN a BaseEnum class - WHEN list_values is called - THEN it returns a list of values - """ - - # GIVEN - class TestEnum(BaseEnum): - FOO = "foo" - BAR = "bar" - FOOBAR = "foobar" - - # WHEN - result = TestEnum.list() - - # THEN - assert result == ["foo", "bar", "foobar"] diff --git a/tests/unit/entrypoint/test_dependencies.py b/tests/unit/entrypoint/test_dependencies.py new file mode 100644 index 0000000..c0a9854 --- /dev/null +++ b/tests/unit/entrypoint/test_dependencies.py @@ -0,0 +1,26 @@ +"""Unit tests for shared entrypoint dependencies.""" + +from types import SimpleNamespace + +from template.entrypoint.dependencies import get_container + + +class TestGetContainer: + """Test cases for the container dependency resolver.""" + + def test_returns_container_from_request_state(self): + """ + GIVEN a request whose app holds a container in state + WHEN get_container is called with that request + THEN it returns the container stored on the app state + """ + + # GIVEN + container = object() + request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(container=container))) + + # WHEN + resolved = get_container(request) # type: ignore[arg-type] + + # THEN + assert resolved is container diff --git a/tests/unit/service_layer/test_handlers.py b/tests/unit/service_layer/test_handlers.py index 12188c8..a9a1f0f 100644 --- a/tests/unit/service_layer/test_handlers.py +++ b/tests/unit/service_layer/test_handlers.py @@ -1,12 +1,20 @@ """Test suite for user application handlers.""" -from uuid import UUID +from uuid import UUID, uuid4 import pytest -from template.domain.commands.user import RegisterUser +from template.domain.commands.user import DeactivateUser, RegisterUser +from template.domain.events.user import UserDeactivated, UserRegistered from template.domain.models.user import User -from template.service_layer.handlers import EmailAlreadyRegistered, register_user +from template.service_layer.handlers import ( + EmailAlreadyRegistered, + UserNotFound, + deactivate_user, + publish_user_deactivated, + publish_user_registered, + register_user, +) from template.service_layer.unit_of_work import AbstractUnitOfWork, IntegrityConflict @@ -16,27 +24,30 @@ class FakeUserRepository: def __init__(self, users: list[User] | None = None): """Initialize the fake repository.""" self.users = users or [] - self.seen: list[User] = [] + self.seen: dict[UUID, User] = {} def add(self, user: User) -> None: """Add a user.""" self.users.append(user) - self.seen.append(user) + self.seen[user.id] = user def get(self, user_id: UUID) -> User | None: """Return a user by identity.""" user = next((user for user in self.users if user.id == user_id), None) if user: - self.seen.append(user) + self.seen[user.id] = user return user def get_by_email(self, email: str) -> User | None: """Return a user by normalized email.""" user = next((user for user in self.users if user.email == email.strip().lower()), None) if user: - self.seen.append(user) + self.seen[user.id] = user return user + def persist_changes(self) -> None: + """Write tracked aggregates back (no-op for the in-memory fake).""" + class FakeUnitOfWork(AbstractUnitOfWork): """Provide an in-memory transaction boundary.""" @@ -107,3 +118,95 @@ def test_translates_a_concurrent_persistence_conflict(self): with pytest.raises(EmailAlreadyRegistered): register_user(RegisterUser(name="Ada Lovelace", email="ada@example.com"), uow) assert uow.rolled_back + + +class TestDeactivateUser: + """Test cases for deactivation orchestration.""" + + def test_deactivates_a_user_and_commits(self): + """ + GIVEN an active user + WHEN the deactivation handler executes + THEN the user is deactivated, the event is recorded, and work commits + """ + # GIVEN + user = User.register(name="Ada Lovelace", email="ada@example.com") + user.events.clear() + uow = FakeUnitOfWork([user]) + + # WHEN + user_id = deactivate_user(DeactivateUser(user_id=user.id), uow) + + # THEN + assert user_id == user.id + assert user.is_active is False + assert user.events == [UserDeactivated(user_id=user.id)] + assert uow.committed + + def test_rejects_an_unknown_user(self): + """ + GIVEN an empty repository + WHEN the deactivation handler targets an unknown identity + THEN the handler reports the user as not found + """ + # GIVEN + uow = FakeUnitOfWork() + + # WHEN / THEN + with pytest.raises(UserNotFound): + deactivate_user(DeactivateUser(user_id=uuid4()), uow) + + def test_deactivation_is_idempotent(self): + """ + GIVEN an already-inactive user + WHEN the deactivation handler runs again + THEN no further deactivation event is recorded + """ + # GIVEN + user = User.register(name="Ada Lovelace", email="ada@example.com") + user.deactivate() + user.events.clear() + uow = FakeUnitOfWork([user]) + + # WHEN + deactivate_user(DeactivateUser(user_id=user.id), uow) + + # THEN + assert user.is_active is False + assert user.events == [] + + +class TestEventPublishers: + """Test cases for the external event publisher handlers.""" + + def test_publishes_user_registered(self): + """ + GIVEN a user-registered event and a capturing publisher + WHEN the publish handler runs + THEN the event is forwarded to the external publisher + """ + # GIVEN + event = UserRegistered(user_id=uuid4(), email="ada@example.com") + published: list[UserRegistered] = [] + + # WHEN + publish_user_registered(event, published.append) + + # THEN + assert published == [event] + + def test_publishes_user_deactivated(self): + """ + GIVEN a user-deactivated event and a capturing publisher + WHEN the publish handler runs + THEN the event is forwarded to the external publisher + """ + # GIVEN + event = UserDeactivated(user_id=uuid4()) + published: list[UserDeactivated] = [] + + # WHEN + publish_user_deactivated(event, published.append) + + # THEN + assert published == [event] diff --git a/tests/unit/service_layer/test_messagebus.py b/tests/unit/service_layer/test_messagebus.py index 4727184..b3e7ca3 100644 --- a/tests/unit/service_layer/test_messagebus.py +++ b/tests/unit/service_layer/test_messagebus.py @@ -2,14 +2,16 @@ from functools import partial from unittest.mock import patch +from uuid import uuid4 import pytest -from template.domain.commands.user import RegisterUser +from template.domain.commands.user import DeactivateUser, RegisterUser from template.domain.events.user import UserRegistered from template.domain.messages import Message from template.service_layer.handlers import publish_user_registered, register_user -from template.service_layer.messagebus import MessageBus +from template.service_layer.messagebus import MessageBus, UnhandledCommand +from template.service_layer.unit_of_work import AbstractUnitOfWork from tests.unit.service_layer.test_handlers import FakeUnitOfWork @@ -76,3 +78,45 @@ def test_rejects_messages_without_command_or_event_semantics(self): # WHEN / THEN with pytest.raises(TypeError, match="Unsupported message type"): bus.handle(Message()) + + def test_rejects_commands_without_a_registered_handler(self): + """ + GIVEN a message bus with no handler for a command type + WHEN that command is dispatched + THEN the bus raises an explicit unhandled-command error + """ + # GIVEN + bus = MessageBus(uow_factory=FakeUnitOfWork, command_handlers={}, event_handlers={}) + + # WHEN / THEN + with pytest.raises(UnhandledCommand, match="DeactivateUser"): + bus.handle(DeactivateUser(user_id=uuid4())) + + +class TestUnitOfWorkEventCollection: + """Test event collection on the abstract unit of work.""" + + def test_yields_nothing_when_never_entered(self): + """ + GIVEN a unit of work that was never entered and has no repository + WHEN events are collected + THEN the collection yields nothing instead of raising + """ + + # GIVEN + class NeverEnteredUnitOfWork(AbstractUnitOfWork): + """A unit of work whose repository is only created on __enter__.""" + + def commit(self) -> None: + """Do nothing.""" + + def rollback(self) -> None: + """Do nothing.""" + + uow = NeverEnteredUnitOfWork() + + # WHEN + events = list(uow.collect_new_events()) + + # THEN + assert events == [] diff --git a/tests/unit/settings/test_api_settings.py b/tests/unit/settings/test_api_settings.py index cfa6e4b..cf50629 100644 --- a/tests/unit/settings/test_api_settings.py +++ b/tests/unit/settings/test_api_settings.py @@ -41,9 +41,21 @@ def test_api_default_values(self): """ settings = ApplicationSettings() - assert settings.DEBUG is True - assert settings.PROJECT_NAME + assert settings.DEBUG is False + assert settings.PROJECT_NAME == "Cosmic FastAPI" assert settings.PROJECT_DESCRIPTION assert isinstance(settings.PROJECT_LICENSE, LicenseInfo) assert isinstance(settings.PROJECT_CONTACT, ContactInfo) + assert settings.PROJECT_CONTACT.name == "Tomas Sanchez" assert __version__ == settings.VERSION + + def test_cors_origins_default_to_safe_allow_list(self): + """ + GIVEN the application settings + WHEN no CORS override is provided + THEN the default origins are an explicit localhost allow-list, not a wildcard + """ + settings = ApplicationSettings() + + assert settings.BACKEND_CORS_ORIGINS + assert "*" not in settings.BACKEND_CORS_ORIGINS diff --git a/tests/unit/test_asgi.py b/tests/unit/test_asgi.py index 65856af..ce23920 100644 --- a/tests/unit/test_asgi.py +++ b/tests/unit/test_asgi.py @@ -2,7 +2,12 @@ Test suite for ASGI Application """ +from fastapi.testclient import TestClient +from starlette.middleware.cors import CORSMiddleware + from template.asgi import get_application +from template.bootstrap import bootstrap +from template.settings.database_settings import DatabaseSettings class TestASGI: @@ -12,8 +17,52 @@ class TestASGI: def test_get_application(self): """ - GIVEN a FastAPI application + GIVEN an in-memory application container + WHEN the application is initialized and its lifespan runs + THEN the application is returned and resources are disposed on exit + """ + # GIVEN + container = bootstrap(DatabaseSettings(URL="sqlite+pysqlite://", AUTO_CREATE_SCHEMA=True)) + app = get_application(container) + + # WHEN / THEN + assert app is not None + with TestClient(app): + pass + + def test_cors_defaults_disable_credentials_only_for_wildcard(self): + """ + GIVEN the default CORS configuration WHEN the application is initialized - THEN the application is returned + THEN credentials are allowed because origins are an explicit allow-list """ - assert get_application() is not None + # GIVEN + container = bootstrap(DatabaseSettings(URL="sqlite+pysqlite://", AUTO_CREATE_SCHEMA=True)) + app = get_application(container) + + # WHEN + cors = next(m for m in app.user_middleware if m.cls is CORSMiddleware) + + # THEN + allow_origins = cors.kwargs["allow_origins"] + assert isinstance(allow_origins, list) + assert "*" not in allow_origins + assert cors.kwargs["allow_credentials"] is True + + def test_cors_wildcard_origin_disables_credentials(self, monkeypatch): + """ + GIVEN a wildcard CORS origin + WHEN the application is initialized + THEN credentials are disabled to avoid the unsafe wildcard + credentials combo + """ + # GIVEN + monkeypatch.setenv("FASTAPI_BACKEND_CORS_ORIGINS", '["*"]') + container = bootstrap(DatabaseSettings(URL="sqlite+pysqlite://", AUTO_CREATE_SCHEMA=True)) + app = get_application(container) + + # WHEN + cors = next(m for m in app.user_middleware if m.cls is CORSMiddleware) + + # THEN + assert cors.kwargs["allow_origins"] == ["*"] + assert cors.kwargs["allow_credentials"] is False diff --git a/tests/unit/test_bootstrap.py b/tests/unit/test_bootstrap.py index 5f8bd49..738d64c 100644 --- a/tests/unit/test_bootstrap.py +++ b/tests/unit/test_bootstrap.py @@ -22,3 +22,16 @@ def test_skips_schema_creation_by_default(self): # THEN assert container.auto_create_schema is False + + def test_uses_a_regular_pool_for_a_file_backed_database(self): + """ + GIVEN a file-backed (non in-memory) database URL + WHEN the container is composed + THEN the static-pool branch is skipped and a normal engine is built + """ + # GIVEN / WHEN + container = bootstrap(DatabaseSettings(URL="sqlite+pysqlite:///./build/example.db", AUTO_CREATE_SCHEMA=False)) + + # THEN + assert "memory" not in str(container.engine.url) + container.shutdown() diff --git a/tests/unit/utils/__init__.py b/tests/unit/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/uv.lock b/uv.lock index 104fbd0..ce56a2e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = ">=3.10, <4.0" +requires-python = ">=3.13" [[package]] name = "alembic" @@ -9,7 +9,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/35/116797ff14635e496bbda0c168987f5326a6555b09312e9b817e360d1f56/alembic-1.16.2.tar.gz", hash = "sha256:e53c38ff88dadb92eb22f8b150708367db731d58ad7e9d417c9168ab516cbed8", size = 1963563, upload-time = "2025-06-16T18:05:08.566Z" } @@ -31,10 +30,8 @@ name = "anyio" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ @@ -86,38 +83,6 @@ version = "7.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/d1/7b18a2e0d2994e4e108dadf16580ec192e0a9c65f7456ccb82ced059f9bf/coverage-7.9.0.tar.gz", hash = "sha256:1a93b43de2233a7670a8bf2520fed8ebd5eea6a65b47417500a9d882b0533fa2", size = 813385, upload-time = "2025-06-11T23:23:34.004Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/25/c83935ed228bd0ce277a9a92b505a4f67b0b15ba0344680974a77452c5dd/coverage-7.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d494fa4256e3cb161ca1df14a91d2d703c27d60452eb0d4a58bb05f52f676e4", size = 211940, upload-time = "2025-06-11T23:21:47.353Z" }, - { url = "https://files.pythonhosted.org/packages/36/42/c58ca1fec2a346ad12356fac955a9b6d848ab37f632a7cb1bc7476efcf90/coverage-7.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b613efceeabf242978d14e1a65626ec3be67c5261918a82a985f56c2a05475ee", size = 212329, upload-time = "2025-06-11T23:21:50.216Z" }, - { url = "https://files.pythonhosted.org/packages/64/0a/6b61e4348cf7b0a70f7995247cde5cc4b5ef0b61d9718109896c77d9ed0e/coverage-7.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673a4d2cb7ec78e1f2f6f41039f6785f27bca0f6bc0e722b53a58286d12754e1", size = 241447, upload-time = "2025-06-11T23:21:51.757Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1e/5f7060b909352cba70d34be0e34619659c0ddbef426665e036d5d3046b3c/coverage-7.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1edc2244932e9fed92ad14428b9480a97ecd37c970333688bd35048f6472f260", size = 239322, upload-time = "2025-06-11T23:21:53.826Z" }, - { url = "https://files.pythonhosted.org/packages/f5/78/f4ba669c9bf15b537136b663ccb846032cfb73e28b59458ef6899f18fe07/coverage-7.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8b92a7617faa2017bd44c94583830bab8be175722d420501680abc4f5bc794", size = 240467, upload-time = "2025-06-11T23:21:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/79/38/3246ea3ac68dc6f85afac0cb0362d3703647378b9882d55796c71fe83a1a/coverage-7.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f3ca1f128f11812d3baf0a482e7f36ffb856ac1ae14de3b5d1adcfb7af955d", size = 240376, upload-time = "2025-06-11T23:21:57.108Z" }, - { url = "https://files.pythonhosted.org/packages/c0/58/ef1f20afbaf9affe2941e7b077a8cf08075c6e3fe5e1dfc3160908b6a1de/coverage-7.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c30eed34eb8206d9b8c2d0d9fa342fa98e10f34b1e9e1eb05f79ccbf4499c8ff", size = 239046, upload-time = "2025-06-11T23:21:58.709Z" }, - { url = "https://files.pythonhosted.org/packages/09/ba/d510b05b3ca0da8fe746acf8ac815b2d560d6c4d5c4e0f6eafb2ec27dc33/coverage-7.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e6f8e5f125cd8bff33593a484a079305c9f0be911f76c6432f580ade5c1a17", size = 239318, upload-time = "2025-06-11T23:21:59.987Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/328a412e3bd78c049180df3f4374bb13a332ed8731ff66f49578d5ebf98c/coverage-7.9.0-cp310-cp310-win32.whl", hash = "sha256:a1b0317b4a8ff4d3703cd7aa642b4f963a71255abe4e878659f768238fab6602", size = 214430, upload-time = "2025-06-11T23:22:01.663Z" }, - { url = "https://files.pythonhosted.org/packages/db/a5/0e788cc4796989d77bfb6b1c58819edc2c65522926f0c08cfe42d1529f2b/coverage-7.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:512b1ea57a11dfa23b7f3d8fe8690fcf8cd983a70ae4c2c262cf5c972618fa15", size = 215350, upload-time = "2025-06-11T23:22:02.957Z" }, - { url = "https://files.pythonhosted.org/packages/9d/91/721a7df15263babfe89caf535a08bacbadebdef87338cf37d40f7400161b/coverage-7.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:55b7b9df45174956e0f719a56cf60c0cb4a7f155668881d00de6384e2a3402f4", size = 212055, upload-time = "2025-06-11T23:22:04.389Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d6/1f4c1eae67e698a8535ede02a6958a7587d06869d33a9b134ecc0e17ee07/coverage-7.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87bceebbc91a58c9264c43638729fcb45910805b9f86444f93654d988305b3a2", size = 212445, upload-time = "2025-06-11T23:22:06.044Z" }, - { url = "https://files.pythonhosted.org/packages/bd/48/c375a6e6a266efa2d5fbf9b04eac88c87430d1a337b4f383ea8beeeedd44/coverage-7.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81da3b6e289bf9fc7dc159ab6d5222f5330ac6e94a6d06f147ba46e53fa6ec82", size = 245010, upload-time = "2025-06-11T23:22:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/7a/43/ec070ad02a1ee10837555a852b6fa256f8c71a953c209488e027673fc5b6/coverage-7.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b361684a91224d4362879c1b1802168d2435ff76666f1b7ba52fc300ad832dbc", size = 242725, upload-time = "2025-06-11T23:22:08.64Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ff/8b8efbd058dd59b489d9c5e27ba5766e895c396dd3bd1b78bebef9808c5f/coverage-7.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a384ea4f77ac0a7e36c9a805ed95ef10f423bdb68b4e9487646cdf548a6a05", size = 244527, upload-time = "2025-06-11T23:22:10.416Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e7/3863f458a3af009a4817656f5b56fa90c7e363d73fef338601b275e979c4/coverage-7.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:38a5642aa82ea6de0e4331e346f5ba188a9fdb7d727e00199f55031b85135d0a", size = 244174, upload-time = "2025-06-11T23:22:12.046Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f0/2ff1fa06ccd3c3d653e352b10ddeec511b018890b28dbd3c29b6ea3f742e/coverage-7.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8c5ff4ca4890c0b57d3e80850534609493280c0f9e6ea2bd314b10cb8cbd76e0", size = 242227, upload-time = "2025-06-11T23:22:13.438Z" }, - { url = "https://files.pythonhosted.org/packages/32/e2/bae13555436f1d0278e70cfe22a0980eab9809e89361e859c96ffa788cb9/coverage-7.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cd052a0c4727ede06393da3c1df1ae6ef6c079e6bdfefb39079877404b3edc22", size = 242815, upload-time = "2025-06-11T23:22:14.723Z" }, - { url = "https://files.pythonhosted.org/packages/20/7c/e1b5b3313c1e3a5e8f8ced567fee67f18c8f18cebee8af0d69052f445a55/coverage-7.9.0-cp311-cp311-win32.whl", hash = "sha256:f73fd1128165e1d665cb7f863a91d00f073044a672c7dfa04ab400af4d1a9226", size = 214469, upload-time = "2025-06-11T23:22:16.187Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/0034d3ccbb7b8f80b1ce8a927ea06e2ba265bd0ba4a9a95a83026ac78dfd/coverage-7.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd62d62e782d3add529c8e7943f5600efd0d07dadf3819e5f9917edb4acf85d8", size = 215407, upload-time = "2025-06-11T23:22:17.611Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e1/7473bf679a43638c5ccba6228f45f68d33c3b7414ffae757dbb0bb2f1127/coverage-7.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:f75288785cc9a67aff3b04dafd8d0f0be67306018b224d319d23867a161578d6", size = 213778, upload-time = "2025-06-11T23:22:19.217Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6b/7bdef79e79076c7e3303ce2453072528ed13988210fb7a8702bb3d98ea8c/coverage-7.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:969ed1ed0ab0325b50af3204f9024782180e64fb281f5a2952f479ec60a02aba", size = 212252, upload-time = "2025-06-11T23:22:20.662Z" }, - { url = "https://files.pythonhosted.org/packages/08/fe/7e08dd50c3c3cfdbe822ee11e24da9f418983faefb4f5e52fbffae5beeb2/coverage-7.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1abd41781c874e716aaeecb8b27db5f4f2bc568f2ed8d41228aa087d567674f0", size = 212491, upload-time = "2025-06-11T23:22:22.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/65/9793cf61b3e4c5647e70aabd5b9470958ffd341c42f90730beeb4d21af9c/coverage-7.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eb6e99487dffd28c88a4fc2ea4286beaf0207a43388775900c93e56cc5a8ae3", size = 246294, upload-time = "2025-06-11T23:22:23.297Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c9/fc61695132da06a34b27a49e853010a80d66a5534a1dfa770cb38aca71c0/coverage-7.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c425c85ddb62b32d44f83fb20044fe32edceceee1db1f978c062eec020a73ea5", size = 243311, upload-time = "2025-06-11T23:22:24.966Z" }, - { url = "https://files.pythonhosted.org/packages/62/0e/559a86887580d0de390e018bddfa632ae0762eeeb065bb5557f319071527/coverage-7.9.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0a1f7676bc90ceba67caa66850d689947d586f204ccf6478400c2bf39da5790", size = 245503, upload-time = "2025-06-11T23:22:26.316Z" }, - { url = "https://files.pythonhosted.org/packages/45/09/344d012dc91e60b8c7afee11ffae18338780c703a5b5fb32d8d82987e7cb/coverage-7.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f17055c50768d710d6abc789c9469d0353574780935e1381b83e63edc49ff530", size = 245313, upload-time = "2025-06-11T23:22:27.936Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2d/151b23e82aaea28aa7e3c0390d893bd1aef685866132aad36034f7d462b8/coverage-7.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:298d2917a6bfadbb272e08545ed026af3965e4d2fe71e3f38bf0a816818b226e", size = 243495, upload-time = "2025-06-11T23:22:29.72Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/0da7fd4ad44259b4b61bd429dc642c6511314a356ffa782b924bd1ea9e5c/coverage-7.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d9be5d26e5f817d478506e4d3c4ff7b92f17d980670b4791bf05baaa37ce2f88", size = 244727, upload-time = "2025-06-11T23:22:31.112Z" }, - { url = "https://files.pythonhosted.org/packages/de/08/6ccf2847c5c0d8fcc153bd8f4341d89ab50c85e01a15cabe4a546d3e943e/coverage-7.9.0-cp312-cp312-win32.whl", hash = "sha256:dc2784edd9ac9fe8692fc5505667deb0b05d895c016aaaf641031ed4a5f93d53", size = 214636, upload-time = "2025-06-11T23:22:33.257Z" }, - { url = "https://files.pythonhosted.org/packages/79/fa/ae2c14d49475215372772f7638c333deaaacda8f3c5717a75377d1992c82/coverage-7.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:18223198464a6d5549db1934cf77a15deb24bb88652c4f5f7cb21cd3ad853704", size = 215448, upload-time = "2025-06-11T23:22:35.125Z" }, - { url = "https://files.pythonhosted.org/packages/62/a9/45309219ba08b89cae84b2cb4ccfed8f941850aa7721c4914282fb3c1081/coverage-7.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:3b00194ff3c84d4b821822ff6c041f245fc55d0d5c7833fc4311d082e97595e8", size = 213817, upload-time = "2025-06-11T23:22:36.557Z" }, { url = "https://files.pythonhosted.org/packages/0b/59/449eb05f795d0050007b57a4efee79b540fa6fcccad813a191351964a001/coverage-7.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:122c60e92ab66c9c88e17565f67a91b3b3be5617cb50f73cfd34a4c60ed4aab0", size = 212271, upload-time = "2025-06-11T23:22:38.305Z" }, { url = "https://files.pythonhosted.org/packages/e0/3b/26852a4fb719a6007b0169c1b52116ed14b61267f0bf3ba1e23db516f352/coverage-7.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:813c11b367a6b3cf37212ec36b230f8d086c22b69dbf62877b40939fb2c79e74", size = 212538, upload-time = "2025-06-11T23:22:39.665Z" }, { url = "https://files.pythonhosted.org/packages/f6/80/99f82896119f36984a5b9189e71c7310fc036613276560b5884b5ee890d7/coverage-7.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f05e0f5e87f23d43fefe49e86655c6209dd4f9f034786b983e6803cf4554183", size = 245705, upload-time = "2025-06-11T23:22:41.103Z" }, @@ -140,15 +105,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/49/1d0120cfa24e001e0d38795388914183c48cd86fc8640ca3b01337831917/coverage-7.9.0-cp313-cp313t-win32.whl", hash = "sha256:c5cbf3ddfb68de8dc8ce33caa9321df27297a032aeaf2e99b278f183fb4ebc37", size = 215349, upload-time = "2025-06-11T23:23:09.037Z" }, { url = "https://files.pythonhosted.org/packages/9f/48/7625c09621a206fff0b51fcbcf5d6c1162ab10a5ffa546fc132f01c9132b/coverage-7.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e3ec9e1525eb7a0f89d31083539b398d921415d884e9f55400002a1e9fe0cf63", size = 216516, upload-time = "2025-06-11T23:23:11.083Z" }, { url = "https://files.pythonhosted.org/packages/bb/50/048b55c34985c3aafcecb32cced3abc4291969bfd967dbcaed95cfc26b2a/coverage-7.9.0-cp313-cp313t-win_arm64.whl", hash = "sha256:a02efe6769f74245ce476e89db3d4e110db07b4c0c3d3f81728e2464bbbbcb8e", size = 214308, upload-time = "2025-06-11T23:23:12.522Z" }, - { url = "https://files.pythonhosted.org/packages/e8/b6/d16966f9439ccc3007e1740960d241420d6ba81502642a4be1da1672a103/coverage-7.9.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:ccf1540a0e82ff525844880f988f6caaa2d037005e57bfe203b71cac7626145d", size = 203927, upload-time = "2025-06-11T23:23:30.913Z" }, { url = "https://files.pythonhosted.org/packages/70/0d/534c1e35cb7688b5c40de93fcca07e3ddc0287659ff85cd376b1dd3f770f/coverage-7.9.0-py3-none-any.whl", hash = "sha256:79ea9a26b27c963cdf541e1eb9ac05311b012bc367d0e31816f1833b06c81c02", size = 203917, upload-time = "2025-06-11T23:23:32.413Z" }, ] -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "distlib" version = "0.3.9" @@ -180,18 +139,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - [[package]] name = "fastapi" version = "0.115.12" @@ -221,33 +168,6 @@ version = "3.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977, upload-time = "2025-06-05T16:10:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351, upload-time = "2025-06-05T16:38:50.685Z" }, - { url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599, upload-time = "2025-06-05T16:41:34.057Z" }, - { url = "https://files.pythonhosted.org/packages/30/64/e01a8261d13c47f3c082519a5e9dbf9e143cc0498ed20c911d04e54d526c/greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", size = 634482, upload-time = "2025-06-05T16:48:16.26Z" }, - { url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284, upload-time = "2025-06-05T16:13:01.599Z" }, - { url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206, upload-time = "2025-06-05T16:12:48.51Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412, upload-time = "2025-06-05T16:36:45.479Z" }, - { url = "https://files.pythonhosted.org/packages/05/46/ab58828217349500a7ebb81159d52ca357da747ff1797c29c6023d79d798/greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", size = 1135054, upload-time = "2025-06-05T16:12:36.478Z" }, - { url = "https://files.pythonhosted.org/packages/68/7f/d1b537be5080721c0f0089a8447d4ef72839039cdb743bdd8ffd23046e9a/greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", size = 296573, upload-time = "2025-06-05T16:34:26.521Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" }, - { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" }, - { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" }, - { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" }, - { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" }, - { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" }, - { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, - { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, @@ -348,36 +268,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, @@ -481,47 +371,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, @@ -539,24 +388,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] [[package]] @@ -605,12 +436,10 @@ version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } wheels = [ @@ -622,7 +451,7 @@ name = "pytest-cov" version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -646,33 +475,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, @@ -686,27 +488,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, - { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, - { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, - { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, - { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, - { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +version = "0.12.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, + { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, + { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, ] [[package]] @@ -728,30 +531,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/12/d7c445b1940276a828efce7331cb0cb09d6e5f049651db22f4ebb0922b77/sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", size = 2117967, upload-time = "2025-05-14T17:48:15.841Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b8/cb90f23157e28946b27eb01ef401af80a1fab7553762e87df51507eaed61/sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", size = 2107583, upload-time = "2025-05-14T17:48:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/eef84283a1c8164a207d898e063edf193d36a24fb6a5bb3ce0634b92a1e8/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", size = 3186025, upload-time = "2025-05-14T17:51:51.226Z" }, - { url = "https://files.pythonhosted.org/packages/bd/72/49d52bd3c5e63a1d458fd6d289a1523a8015adedbddf2c07408ff556e772/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", size = 3186259, upload-time = "2025-05-14T17:55:22.526Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9e/e3ffc37d29a3679a50b6bbbba94b115f90e565a2b4545abb17924b94c52d/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", size = 3126803, upload-time = "2025-05-14T17:51:53.277Z" }, - { url = "https://files.pythonhosted.org/packages/8a/76/56b21e363f6039978ae0b72690237b38383e4657281285a09456f313dd77/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", size = 3148566, upload-time = "2025-05-14T17:55:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/3b/92/11b8e1b69bf191bc69e300a99badbbb5f2f1102f2b08b39d9eee2e21f565/sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda", size = 2086696, upload-time = "2025-05-14T17:55:59.136Z" }, - { url = "https://files.pythonhosted.org/packages/5c/88/2d706c9cc4502654860f4576cd54f7db70487b66c3b619ba98e0be1a4642/sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08", size = 2110200, upload-time = "2025-05-14T17:56:00.757Z" }, - { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" }, - { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" }, - { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" }, - { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" }, - { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, - { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, - { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, - { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, @@ -790,7 +569,7 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "httpx" }, { name = "pre-commit" }, { name = "pyrefly" }, @@ -817,46 +596,7 @@ dev = [ { name = "pyrefly", specifier = ">=1.0.0" }, { name = "pytest", specifier = ">=8.4.0" }, { name = "pytest-cov", specifier = ">=6.2.0" }, - { name = "ruff" }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { name = "ruff", specifier = ">=0.12,<0.13" }, ] [[package]] @@ -887,7 +627,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } wheels = [