mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-08-05 16:14:08 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
518e22e921 | ||
|
|
88ecfa8486
|
||
|
|
b4a7335e5e | ||
|
|
97828cc156 | ||
|
|
e7540c6e27 | ||
|
|
7779dcbadd | ||
|
|
ea354813d1 | ||
|
|
fd83a7142d | ||
|
|
a407677be0 | ||
|
|
ea5b5695b5 | ||
|
|
651326d54b | ||
|
|
169bf710f0 | ||
|
|
adc0ff14c1 | ||
|
|
19a823dc8b | ||
|
|
c15aee22b0 | ||
|
|
7da08ffd9a | ||
|
|
51acf61d0f | ||
|
|
436caa3141 | ||
|
|
a0d51e043d | ||
|
|
507f3d99d1 | ||
|
|
a49d256f17 | ||
|
|
03433f53b1 |
+10
-3
@@ -85,13 +85,17 @@ The `paginate` shorthand was an alias for `offset_paginate`. It has been removed
|
||||
=== "Before (`v1`)"
|
||||
|
||||
```python
|
||||
result = await UserCrud.paginate(session=session, page=2, items_per_page=20, schema=UserRead)
|
||||
result = await UserCrud.paginate(
|
||||
session=session, page=2, items_per_page=20, schema=UserRead
|
||||
)
|
||||
```
|
||||
|
||||
=== "Now (`v2`)"
|
||||
|
||||
```python
|
||||
result = await UserCrud.offset_paginate(session=session, page=2, items_per_page=20, schema=UserRead)
|
||||
result = await UserCrud.offset_paginate(
|
||||
session=session, page=2, items_per_page=20, schema=UserRead
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -124,8 +128,11 @@ For shared base classes that are not meant to be raised directly, use `abstract=
|
||||
class BillingError(ApiException, abstract=True):
|
||||
"""Base for all billing-related errors — not raised directly."""
|
||||
|
||||
|
||||
class PaymentRequiredError(BillingError):
|
||||
api_error = ApiError(code=402, msg="Payment Required", desc="...", err_code="BILLING-402")
|
||||
api_error = ApiError(
|
||||
code=402, msg="Payment Required", desc="...", err_code="BILLING-402"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+13
-2
@@ -45,12 +45,16 @@ Each new method accepts `search`, `filter`, and `order` boolean toggles (all `Tr
|
||||
```python
|
||||
from fastapi_toolsets.crud import OrderByClause
|
||||
|
||||
|
||||
@router.get("/offset")
|
||||
async def list_articles_offset(
|
||||
session: SessionDep,
|
||||
params: Annotated[dict, Depends(ArticleCrud.offset_params(default_page_size=20))],
|
||||
filter_by: Annotated[dict, Depends(ArticleCrud.filter_params())],
|
||||
order_by: Annotated[OrderByClause | None, Depends(ArticleCrud.order_params(default_field=Article.created_at))],
|
||||
order_by: Annotated[
|
||||
OrderByClause | None,
|
||||
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
|
||||
],
|
||||
search: str | None = None,
|
||||
) -> OffsetPaginatedResponse[ArticleRead]:
|
||||
return await ArticleCrud.offset_paginate(
|
||||
@@ -79,7 +83,9 @@ Each new method accepts `search`, `filter`, and `order` boolean toggles (all `Tr
|
||||
),
|
||||
],
|
||||
) -> OffsetPaginatedResponse[ArticleRead]:
|
||||
return await ArticleCrud.offset_paginate(session=session, **params, schema=ArticleRead)
|
||||
return await ArticleCrud.offset_paginate(
|
||||
session=session, **params, schema=ArticleRead
|
||||
)
|
||||
```
|
||||
|
||||
The same pattern applies to `cursor_paginate_params()` and `paginate_params()`. To disable a feature, pass the toggle:
|
||||
@@ -109,6 +115,7 @@ Model method callbacks (`on_create`, `on_delete`, `on_update`) and the `@watch`
|
||||
```python
|
||||
from fastapi_toolsets.models import WatchedFieldsMixin, watch
|
||||
|
||||
|
||||
@watch("status")
|
||||
class Order(Base, UUIDMixin, WatchedFieldsMixin):
|
||||
__tablename__ = "orders"
|
||||
@@ -131,21 +138,25 @@ Model method callbacks (`on_create`, `on_delete`, `on_update`) and the `@watch`
|
||||
```python
|
||||
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for
|
||||
|
||||
|
||||
class Order(Base, UUIDMixin):
|
||||
__tablename__ = "orders"
|
||||
__watched_fields__ = ("status",)
|
||||
|
||||
status: Mapped[str]
|
||||
|
||||
|
||||
@listens_for(Order, [ModelEvent.CREATE])
|
||||
async def on_order_created(order: Order, event_type: ModelEvent, changes: None):
|
||||
await notify_new_order(order.id)
|
||||
|
||||
|
||||
@listens_for(Order, [ModelEvent.UPDATE])
|
||||
async def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):
|
||||
if "status" in changes:
|
||||
await notify_status_change(order.id, changes["status"])
|
||||
|
||||
|
||||
@listens_for(Order, [ModelEvent.DELETE])
|
||||
async def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):
|
||||
await notify_order_cancelled(order.id)
|
||||
|
||||
@@ -35,6 +35,8 @@ The function creates and manages its own **dedicated session** internally, yield
|
||||
user.balance += 100
|
||||
|
||||
# With a custom lock mode
|
||||
async with lock_tables(session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE) as session:
|
||||
async with lock_tables(
|
||||
session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE
|
||||
) as session:
|
||||
await process_order(session, order_id)
|
||||
```
|
||||
|
||||
+26
-7
@@ -24,9 +24,10 @@ Build one `Database` with your URL (or an existing `engine=`), then use the inst
|
||||
get_db = create_db_dependency(session_maker=SessionLocal)
|
||||
get_db_context = create_db_context(session_maker=SessionLocal)
|
||||
|
||||
|
||||
@app.get("/users")
|
||||
async def list_users(session: AsyncSession = Depends(get_db)):
|
||||
...
|
||||
async def list_users(session: AsyncSession = Depends(get_db)): ...
|
||||
|
||||
|
||||
async def seed():
|
||||
async with get_db_context() as session:
|
||||
@@ -40,9 +41,10 @@ Build one `Database` with your URL (or an existing `engine=`), then use the inst
|
||||
|
||||
db = Database(url="postgresql+asyncpg://...")
|
||||
|
||||
|
||||
@app.get("/users")
|
||||
async def list_users(session: AsyncSession = Depends(db)):
|
||||
...
|
||||
async def list_users(session: AsyncSession = Depends(db)): ...
|
||||
|
||||
|
||||
async def seed():
|
||||
async with db.session() as session:
|
||||
@@ -89,7 +91,9 @@ The free `lock_tables(session_maker, tables, ...)` function still exists for cal
|
||||
```python
|
||||
from fastapi_toolsets.db import lock_tables, LockMode
|
||||
|
||||
async with lock_tables(session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE) as session:
|
||||
async with lock_tables(
|
||||
session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE
|
||||
) as session:
|
||||
await process_order(session, order_id)
|
||||
```
|
||||
|
||||
@@ -120,14 +124,18 @@ The free `lock_tables(session_maker, tables, ...)` function still exists for cal
|
||||
|
||||
### `get_obj_by_attr` / `get_field_by_attr` are now `FixtureRegistry` methods
|
||||
|
||||
Both also change their first argument: instead of the fixture *function*, pass the fixture's *registered name* and let the registry look it up.
|
||||
|
||||
=== "Before (`v4`)"
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.fixtures import get_obj_by_attr, get_field_by_attr
|
||||
|
||||
|
||||
@fixtures.register(depends_on=["roles"])
|
||||
def users():
|
||||
admin_role = get_obj_by_attr(roles, "name", "admin")
|
||||
admin_role = get_obj_by_attr(fixtures=roles, attr_name="name", value="admin")
|
||||
admin_role_id = get_field_by_attr(fixtures=roles, attr_name="name", value="admin")
|
||||
return [User(id=1, username="alice", role_id=admin_role.id)]
|
||||
```
|
||||
|
||||
@@ -136,10 +144,21 @@ The free `lock_tables(session_maker, tables, ...)` function still exists for cal
|
||||
```python
|
||||
@fixtures.register(depends_on=["roles"])
|
||||
def users():
|
||||
admin_role = fixtures.obj("roles", "name", "admin")
|
||||
admin_role = fixtures.obj(name="roles", attr_name="name", value="admin")
|
||||
admin_role_id = fixtures.field(name="roles", attr_name="name", value="admin")
|
||||
return [User(id=1, username="alice", role_id=admin_role.id)]
|
||||
```
|
||||
|
||||
### Loading/listing by context now always includes `Context.BASE`
|
||||
|
||||
`FixtureRegistry.get_variants`/`get_by_context`, `load_fixtures_by_context`, and the `fixtures list`/`fixtures load` CLI commands now implicitly union every queried context with `Context.BASE`. In `v4`, requesting a single context (e.g. `"testing"`) returned only fixtures tagged with that context; in `v5` it also returns every `Context.BASE`-tagged fixture.
|
||||
|
||||
If you relied on strict exclusion of base fixtures, move them out of `Context.BASE` into their own context.
|
||||
|
||||
### `load_fixtures` / `load_fixtures_by_context` now return reloaded instances
|
||||
|
||||
Loaded objects are re-queried from the database after insert, with all relationships eager-loaded, instead of returning the exact objects your fixture function constructed. Code that compares returned objects by identity to the fixture function's output, or that expected relationships to remain unloaded, should be updated — and expect one extra query per model type per load.
|
||||
|
||||
## Security
|
||||
|
||||
The security module has been removed and moved to a dedicated python package: [`fastapi-multiauth`](https://github.com/d3vyce/fastapi-multiauth).
|
||||
|
||||
+2
-1
@@ -92,6 +92,7 @@ import typer
|
||||
|
||||
cli = typer.Typer()
|
||||
|
||||
|
||||
@cli.command()
|
||||
def hello():
|
||||
print("Hello from my app!")
|
||||
@@ -99,7 +100,7 @@ def hello():
|
||||
|
||||
```toml
|
||||
[tool.fastapi-toolsets]
|
||||
cli = "myapp.cli:cli"
|
||||
custom_cli = "myapp.cli:cli"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+36
-6
@@ -30,6 +30,7 @@ UserCrud = CrudFactory(model=User)
|
||||
from fastapi_toolsets.crud.factory import AsyncCrud
|
||||
from myapp.models import User
|
||||
|
||||
|
||||
class UserCrud(AsyncCrud[User]):
|
||||
model = User
|
||||
searchable_fields = [User.username, User.email]
|
||||
@@ -61,6 +62,7 @@ from fastapi_toolsets.crud.factory import AsyncCrud
|
||||
|
||||
T = TypeVar("T", bound=DeclarativeBase)
|
||||
|
||||
|
||||
class AuditedCrud(AsyncCrud[T], Generic[T]):
|
||||
"""Base CRUD with custom function"""
|
||||
|
||||
@@ -101,7 +103,9 @@ user = await UserCrud.first(session=session, filters=[User.email == email])
|
||||
users = await UserCrud.get_multi(session=session, filters=[User.is_active == True])
|
||||
|
||||
# Update
|
||||
user = await UserCrud.update(session=session, obj=UserUpdateSchema(username="bob"), filters=[User.id == user_id])
|
||||
user = await UserCrud.update(
|
||||
session=session, obj=UserUpdateSchema(username="bob"), filters=[User.id == user_id]
|
||||
)
|
||||
|
||||
# Delete
|
||||
await UserCrud.delete(session=session, filters=[User.id == user_id])
|
||||
@@ -160,10 +164,14 @@ user = await UserCrud.get(session, [User.id == user_id], with_for_update=True)
|
||||
user = await UserCrud.get(session, [User.id == user_id], with_for_update="nowait")
|
||||
|
||||
# Skip rows already locked by another transaction (e.g. job queues)
|
||||
rows = await JobCrud.get_multi(session, filters=[Job.status == "pending"], with_for_update="skip_locked")
|
||||
rows = await JobCrud.get_multi(
|
||||
session, filters=[Job.status == "pending"], with_for_update="skip_locked"
|
||||
)
|
||||
|
||||
# Lock atomically as part of update (prevents race between SELECT and UPDATE)
|
||||
user = await UserCrud.update(session, UserUpdate(credits=10), [User.id == user_id], with_for_update=True)
|
||||
user = await UserCrud.update(
|
||||
session, UserUpdate(credits=10), [User.id == user_id], with_for_update=True
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
@@ -193,6 +201,7 @@ Three pagination methods are available. All return a typed response whose `pagi
|
||||
from typing import Annotated
|
||||
from fastapi import Depends
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_users(
|
||||
session: SessionDep,
|
||||
@@ -228,7 +237,9 @@ By default `offset_paginate` runs two queries: one for the page items and one `C
|
||||
@router.get("")
|
||||
async def get_users(
|
||||
session: SessionDep,
|
||||
params: Annotated[dict, Depends(UserCrud.offset_paginate_params(include_total=False))],
|
||||
params: Annotated[
|
||||
dict, Depends(UserCrud.offset_paginate_params(include_total=False))
|
||||
],
|
||||
) -> OffsetPaginatedResponse[UserRead]:
|
||||
return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)
|
||||
```
|
||||
@@ -303,6 +314,7 @@ PostCrud = CrudFactory(model=Post, cursor_column=Post.created_at)
|
||||
```python
|
||||
from fastapi_toolsets.schemas import PaginatedResponse
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_users(
|
||||
session: SessionDep,
|
||||
@@ -446,6 +458,7 @@ from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
|
||||
@router.get("", response_model_exclude_none=True)
|
||||
async def list_users(
|
||||
session: SessionDep,
|
||||
@@ -491,6 +504,16 @@ The distinct values for each facet field are returned in the `filter_attributes`
|
||||
!!! info "Key format uses `__` as a separator for relationship chains."
|
||||
A direct column `User.status` produces `"status"`. A relationship tuple `(User.role, Role.name)` produces `"role__name"`. A deeper chain `(User.role, Role.permission, Permission.name)` produces `"role__permission__name"`. An unknown `filter_by` key raises [`InvalidFacetFilterError`](../reference/exceptions.md#fastapi_toolsets.exceptions.exceptions.InvalidFacetFilterError) (HTTP 422).
|
||||
|
||||
#### Skipping facet queries
|
||||
|
||||
!!! info "Added in `v5.1.0`"
|
||||
|
||||
Facet values only change with the filters, not with the page. Pass `include_facets=False` to `offset_paginate_params()` / `cursor_paginate_params()` on pages 2..N to skip the facet queries entirely (`filter_attributes` will be `None`):
|
||||
|
||||
```python
|
||||
params: Annotated[dict, Depends(UserCrud.offset_paginate_params(include_facets=False))]
|
||||
```
|
||||
|
||||
## Sorting
|
||||
|
||||
!!! info "Added in `v1.3`"
|
||||
@@ -530,6 +553,7 @@ from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_users(
|
||||
session: SessionDep,
|
||||
@@ -622,7 +646,9 @@ PostCrud = CrudFactory(
|
||||
m2m_fields={"tag_ids": Post.tags},
|
||||
)
|
||||
|
||||
post = await PostCrud.create(session=session, obj=PostCreateSchema(title="Hello", tag_ids=[1, 2, 3]))
|
||||
post = await PostCrud.create(
|
||||
session=session, obj=PostCreateSchema(title="Hello", tag_ids=[1, 2, 3])
|
||||
)
|
||||
```
|
||||
|
||||
## Upsert
|
||||
@@ -649,6 +675,7 @@ class UserRead(PydanticBase):
|
||||
id: UUID
|
||||
username: str
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{uuid}",
|
||||
responses=generate_error_responses(NotFoundError),
|
||||
@@ -660,12 +687,15 @@ async def get_user(session: SessionDep, uuid: UUID) -> Response[UserRead]:
|
||||
schema=UserRead,
|
||||
)
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_users(
|
||||
session: SessionDep,
|
||||
params: Annotated[dict, Depends(crud.UserCrud.offset_paginate_params())],
|
||||
) -> OffsetPaginatedResponse[UserRead]:
|
||||
return await crud.UserCrud.offset_paginate(session=session, **params, schema=UserRead)
|
||||
return await crud.UserCrud.offset_paginate(
|
||||
session=session, **params, schema=UserRead
|
||||
)
|
||||
```
|
||||
|
||||
The schema must have `from_attributes=True` (or inherit from [`PydanticBase`](../reference/schemas.md#fastapi_toolsets.schemas.PydanticBase)) so it can be built from SQLAlchemy model instances.
|
||||
|
||||
+8
-2
@@ -24,9 +24,9 @@ db = Database("postgresql+asyncpg://postgres:postgres@localhost/app")
|
||||
app = FastAPI()
|
||||
db.install(app) # commit middleware + engine disposal on shutdown
|
||||
|
||||
|
||||
@app.get("/users")
|
||||
async def list_users(session: AsyncSession = Depends(db)):
|
||||
...
|
||||
async def list_users(session: AsyncSession = Depends(db)): ...
|
||||
```
|
||||
|
||||
The `Database` instance **is** the dependency: use it directly as `Depends(db)`. The whole request runs as a single transaction (CRUD writes use savepoints under it).
|
||||
@@ -37,9 +37,11 @@ The **URL** may be a plain string or a Pydantic [`PostgresDsn`](https://docs.pyd
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import PostgresDsn
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
database_url: PostgresDsn
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
db = Database(
|
||||
@@ -72,12 +74,14 @@ Without `install`, the session commits in the dependency teardown, which runs af
|
||||
```python
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
await warm_cache() # your startup
|
||||
yield
|
||||
await flush_metrics() # your shutdown
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
db.install(app) # your shutdown runs first, then the engine is disposed
|
||||
```
|
||||
@@ -101,6 +105,7 @@ async def seed():
|
||||
```python
|
||||
from fastapi_toolsets.db import transaction
|
||||
|
||||
|
||||
async def create_user_with_role(session):
|
||||
async with transaction(session):
|
||||
...
|
||||
@@ -207,6 +212,7 @@ For test isolation with automatic cleanup, use [`create_worker_database`](../ref
|
||||
```python
|
||||
from fastapi_toolsets.db.testing import cleanup_tables
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def clean(db_session):
|
||||
yield
|
||||
|
||||
@@ -20,6 +20,7 @@ UserDep = PathDependency(model=User, field=User.id, session_dep=get_db)
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_db)]
|
||||
UserDep = PathDependency(model=User, field=User.id, session_dep=SessionDep)
|
||||
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(user: User = UserDep):
|
||||
return user
|
||||
@@ -30,6 +31,7 @@ By default the parameter name is inferred from the field (`user_id` for `User.id
|
||||
```python
|
||||
UserDep = PathDependency(model=User, field=User.id, session_dep=get_db, param_name="id")
|
||||
|
||||
|
||||
@router.get("/users/{id}")
|
||||
async def get_user(user: User = UserDep):
|
||||
return user
|
||||
@@ -43,11 +45,15 @@ async def get_user(user: User = UserDep):
|
||||
from fastapi_toolsets.dependencies import BodyDependency
|
||||
|
||||
# Plain callable
|
||||
RoleDep = BodyDependency(model=Role, field=Role.id, session_dep=get_db, body_field="role_id")
|
||||
RoleDep = BodyDependency(
|
||||
model=Role, field=Role.id, session_dep=get_db, body_field="role_id"
|
||||
)
|
||||
|
||||
# Annotated
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_db)]
|
||||
RoleDep = BodyDependency(model=Role, field=Role.id, session_dep=SessionDep, body_field="role_id")
|
||||
RoleDep = BodyDependency(
|
||||
model=Role, field=Role.id, session_dep=SessionDep, body_field="role_id"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/users")
|
||||
|
||||
@@ -53,7 +53,9 @@ All built-in exceptions accept optional keyword arguments to customise the respo
|
||||
| `data` | Overrides the `data` field |
|
||||
|
||||
```python
|
||||
raise NotFoundError(detail="User 42 not found", desc="No user with that ID exists in the database.")
|
||||
raise NotFoundError(
|
||||
detail="User 42 not found", desc="No user with that ID exists in the database."
|
||||
)
|
||||
```
|
||||
|
||||
## Custom exceptions
|
||||
@@ -64,6 +66,7 @@ Subclass [`ApiException`](../reference/exceptions.md#fastapi_toolsets.exceptions
|
||||
from fastapi_toolsets.exceptions import ApiException
|
||||
from fastapi_toolsets.schemas import ApiError
|
||||
|
||||
|
||||
class PaymentRequiredError(ApiException):
|
||||
api_error = ApiError(
|
||||
code=402,
|
||||
@@ -105,11 +108,17 @@ Use `abstract=True` when creating a shared base that is not meant to be raised d
|
||||
class BillingError(ApiException, abstract=True):
|
||||
"""Base for all billing-related errors."""
|
||||
|
||||
|
||||
class PaymentRequiredError(BillingError):
|
||||
api_error = ApiError(code=402, msg="Payment Required", desc="...", err_code="BILLING-402")
|
||||
api_error = ApiError(
|
||||
code=402, msg="Payment Required", desc="...", err_code="BILLING-402"
|
||||
)
|
||||
|
||||
|
||||
class SubscriptionExpiredError(BillingError):
|
||||
api_error = ApiError(code=402, msg="Subscription Expired", desc="...", err_code="BILLING-402-EXP")
|
||||
api_error = ApiError(
|
||||
code=402, msg="Subscription Expired", desc="...", err_code="BILLING-402-EXP"
|
||||
)
|
||||
```
|
||||
|
||||
## OpenAPI response documentation
|
||||
|
||||
+17
-4
@@ -13,6 +13,7 @@ from fastapi_toolsets.fixtures import FixtureRegistry, Context
|
||||
|
||||
fixtures = FixtureRegistry()
|
||||
|
||||
|
||||
@fixtures.register
|
||||
def roles():
|
||||
return [
|
||||
@@ -20,6 +21,7 @@ def roles():
|
||||
Role(id=2, name="user"),
|
||||
]
|
||||
|
||||
|
||||
@fixtures.register(depends_on=["roles"], contexts=[Context.TESTING])
|
||||
def test_users():
|
||||
return [
|
||||
@@ -79,14 +81,17 @@ Plain strings and any `Enum` subclass are accepted wherever a `Context` enum is
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AppContext(str, Enum):
|
||||
STAGING = "staging"
|
||||
DEMO = "demo"
|
||||
|
||||
|
||||
@fixtures.register(contexts=[AppContext.STAGING])
|
||||
def staging_data():
|
||||
return [Config(key="feature_x", enabled=True)]
|
||||
|
||||
|
||||
# loads staging_data plus any Context.BASE fixtures
|
||||
await load_fixtures_by_context(session, fixtures, AppContext.STAGING)
|
||||
```
|
||||
@@ -98,6 +103,7 @@ Pass `contexts` to `FixtureRegistry` to set a default for all fixtures registere
|
||||
```python
|
||||
testing_registry = FixtureRegistry(contexts=[Context.TESTING])
|
||||
|
||||
|
||||
@testing_registry.register # implicitly contexts=[Context.TESTING]
|
||||
def test_orders():
|
||||
return [Order(id=1, total=99)]
|
||||
@@ -112,10 +118,12 @@ The same fixture name may be registered under different (non-overlapping) contex
|
||||
def users():
|
||||
return [User(id=1, username="admin")]
|
||||
|
||||
|
||||
@fixtures.register(contexts=[Context.TESTING])
|
||||
def users():
|
||||
return [User(id=2, username="tester")]
|
||||
|
||||
|
||||
# loads both admin and tester (Context.BASE is included automatically)
|
||||
await load_fixtures_by_context(session, fixtures, Context.TESTING)
|
||||
```
|
||||
@@ -171,7 +179,9 @@ Looking the fixture up by name (instead of importing the `roles` function direct
|
||||
```python
|
||||
@fixtures.register(depends_on=["roles"])
|
||||
def users():
|
||||
return [User(id=1, username="alice", role_id=fixtures.field("roles", "name", "admin"))]
|
||||
return [
|
||||
User(id=1, username="alice", role_id=fixtures.field("roles", "name", "admin"))
|
||||
]
|
||||
```
|
||||
|
||||
Both raise `StopIteration` if no matching instance is found, and `KeyError` if the fixture name isn't registered.
|
||||
@@ -189,18 +199,21 @@ from app.models import Base
|
||||
|
||||
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/test_db"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db_session():
|
||||
async with create_db_session(database_url=DATABASE_URL, base=Base, cleanup=True) as session:
|
||||
async with create_db_session(
|
||||
database_url=DATABASE_URL, base=Base, cleanup=True
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
register_fixtures(registry=registry, namespace=globals())
|
||||
```
|
||||
|
||||
```python
|
||||
# test_users.py
|
||||
async def test_user_can_login(fixture_users: list[User], fixture_roles: list[Role]):
|
||||
...
|
||||
async def test_user_can_login(fixture_users: list[User], fixture_roles: list[Role]): ...
|
||||
```
|
||||
|
||||
The load order is resolved automatically from the `depends_on` declarations in your registry. Each generated fixture receives `db_session` as a dependency and returns the list of loaded model instances.
|
||||
|
||||
@@ -47,10 +47,12 @@ If neither of these applies to you, declaring metrics at module level (e.g. `HTT
|
||||
```python
|
||||
from prometheus_client import Counter, Histogram
|
||||
|
||||
|
||||
@metrics.register
|
||||
def http_requests():
|
||||
return Counter("http_requests_total", "Total HTTP requests", ["method", "status"])
|
||||
|
||||
|
||||
@metrics.register
|
||||
def request_duration():
|
||||
return Histogram("request_duration_seconds", "Request duration")
|
||||
@@ -79,6 +81,7 @@ from prometheus_client import Gauge
|
||||
|
||||
_queue_depth = Gauge("queue_depth", "Current queue depth")
|
||||
|
||||
|
||||
@metrics.register(collect=True)
|
||||
def collect_queue_depth():
|
||||
_queue_depth.set(get_current_queue_depth())
|
||||
|
||||
+19
-1
@@ -11,6 +11,7 @@ The `models` module provides mixins that each add a single, well-defined column
|
||||
```python
|
||||
from fastapi_toolsets.models import UUIDMixin, TimestampMixin
|
||||
|
||||
|
||||
class Article(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "articles"
|
||||
|
||||
@@ -31,11 +32,13 @@ Adds a `id: UUID` primary key generated server-side by PostgreSQL using `gen_ran
|
||||
```python
|
||||
from fastapi_toolsets.models import UUIDMixin
|
||||
|
||||
|
||||
class User(Base, UUIDMixin):
|
||||
__tablename__ = "users"
|
||||
|
||||
username: Mapped[str]
|
||||
|
||||
|
||||
# id is None before flush
|
||||
user = User(username="alice")
|
||||
session.add(user)
|
||||
@@ -54,11 +57,13 @@ Adds a `id: UUID` primary key generated server-side by PostgreSQL using `uuidv7(
|
||||
```python
|
||||
from fastapi_toolsets.models import UUIDv7Mixin
|
||||
|
||||
|
||||
class Event(Base, UUIDv7Mixin):
|
||||
__tablename__ = "events"
|
||||
|
||||
name: Mapped[str]
|
||||
|
||||
|
||||
# id is None before flush
|
||||
event = Event(name="user.signup")
|
||||
session.add(event)
|
||||
@@ -73,6 +78,7 @@ Adds a `created_at: datetime` column set to `clock_timestamp()` on insert. The c
|
||||
```python
|
||||
from fastapi_toolsets.models import UUIDMixin, CreatedAtMixin
|
||||
|
||||
|
||||
class Order(Base, UUIDMixin, CreatedAtMixin):
|
||||
__tablename__ = "orders"
|
||||
|
||||
@@ -86,11 +92,13 @@ Adds an `updated_at: datetime` column set to `clock_timestamp()` on insert and a
|
||||
```python
|
||||
from fastapi_toolsets.models import UUIDMixin, UpdatedAtMixin
|
||||
|
||||
|
||||
class Post(Base, UUIDMixin, UpdatedAtMixin):
|
||||
__tablename__ = "posts"
|
||||
|
||||
title: Mapped[str]
|
||||
|
||||
|
||||
post = Post(title="Hello")
|
||||
await session.flush()
|
||||
await session.refresh(post)
|
||||
@@ -111,6 +119,7 @@ Convenience mixin that combines [`CreatedAtMixin`](../reference/models.md#fastap
|
||||
```python
|
||||
from fastapi_toolsets.models import UUIDMixin, TimestampMixin
|
||||
|
||||
|
||||
class Article(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "articles"
|
||||
|
||||
@@ -166,10 +175,12 @@ class Order(Base, UUIDMixin):
|
||||
__watched_fields__ = ("status",)
|
||||
...
|
||||
|
||||
|
||||
class UrgentOrder(Order):
|
||||
# inherits __watched_fields__ = ("status",)
|
||||
...
|
||||
|
||||
|
||||
class PriorityOrder(Order):
|
||||
__watched_fields__ = ("priority",)
|
||||
# overrides parent — UPDATE fires only for priority changes
|
||||
@@ -183,20 +194,24 @@ Register handlers with the [`listens_for`](../reference/models.md#fastapi_toolse
|
||||
```python
|
||||
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for
|
||||
|
||||
|
||||
class Order(Base, UUIDMixin):
|
||||
__tablename__ = "orders"
|
||||
__watched_fields__ = ("status",)
|
||||
|
||||
status: Mapped[str]
|
||||
|
||||
|
||||
@listens_for(Order, [ModelEvent.CREATE])
|
||||
async def on_order_created(order: Order, event_type: ModelEvent, changes: None):
|
||||
await notify_new_order(order.id)
|
||||
|
||||
|
||||
@listens_for(Order, [ModelEvent.DELETE])
|
||||
async def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):
|
||||
await notify_order_cancelled(order.id)
|
||||
|
||||
|
||||
@listens_for(Order, [ModelEvent.UPDATE])
|
||||
async def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):
|
||||
if "status" in changes:
|
||||
@@ -212,8 +227,11 @@ A single handler can listen for multiple events at once. When `event_types` is o
|
||||
async def on_order_changed(order: Order, event_type: ModelEvent, changes: dict | None):
|
||||
await invalidate_cache(order.id)
|
||||
|
||||
|
||||
@listens_for(Order) # all events
|
||||
async def on_any_order_event(order: Order, event_type: ModelEvent, changes: dict | None):
|
||||
async def on_any_order_event(
|
||||
order: Order, event_type: ModelEvent, changes: dict | None
|
||||
):
|
||||
await audit_log(order.id, event_type)
|
||||
```
|
||||
|
||||
|
||||
+11
-2
@@ -21,6 +21,7 @@ Use [`create_async_client`](../reference/pytest.md#fastapi_toolsets.pytest.utils
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_async_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def http_client(db_session):
|
||||
async def _override_get_db():
|
||||
@@ -52,6 +53,7 @@ Use [`create_worker_database`](../reference/pytest.md#fastapi_toolsets.pytest.ut
|
||||
```python
|
||||
from fastapi_toolsets.pytest import create_worker_database, create_db_session
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def worker_db_url():
|
||||
async with create_worker_database(
|
||||
@@ -96,11 +98,17 @@ Use [`worker_database_url`](../reference/pytest.md#fastapi_toolsets.pytest.utils
|
||||
```python
|
||||
from fastapi_toolsets.pytest import worker_database_url
|
||||
|
||||
url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test")
|
||||
url = worker_database_url(
|
||||
"postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test"
|
||||
)
|
||||
# → "postgresql+asyncpg://user:pass@localhost/gw0" under xdist
|
||||
# → "postgresql+asyncpg://user:pass@localhost/test" otherwise
|
||||
|
||||
url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", default_test_db="test", prefix="myapp")
|
||||
url = worker_database_url(
|
||||
"postgresql+asyncpg://user:pass@localhost/myapp",
|
||||
default_test_db="test",
|
||||
prefix="myapp",
|
||||
)
|
||||
# → "postgresql+asyncpg://user:pass@localhost/myapp_gw0" under xdist
|
||||
# → "postgresql+asyncpg://user:pass@localhost/myapp_test" otherwise
|
||||
```
|
||||
@@ -112,6 +120,7 @@ url = worker_database_url("postgresql+asyncpg://user:pass@localhost/myapp", defa
|
||||
```python
|
||||
from fastapi_toolsets.pytest import cleanup_tables
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def clean(db_session):
|
||||
yield
|
||||
|
||||
@@ -15,6 +15,7 @@ The most common wrapper for a single resource response.
|
||||
```python
|
||||
from fastapi_toolsets.schemas import Response
|
||||
|
||||
|
||||
@router.get("/users/{id}")
|
||||
async def get_user(user: User = UserDep) -> Response[UserSchema]:
|
||||
return Response(data=user, message="User retrieved")
|
||||
@@ -39,6 +40,7 @@ Use as the return type when the endpoint always uses [`offset_paginate`](crud.md
|
||||
```python
|
||||
from fastapi_toolsets.schemas import OffsetPaginatedResponse
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(
|
||||
page: int = 1,
|
||||
@@ -74,6 +76,7 @@ Use as the return type when the endpoint always uses [`cursor_paginate`](crud.md
|
||||
```python
|
||||
from fastapi_toolsets.schemas import CursorPaginatedResponse
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def list_events(
|
||||
cursor: str | None = None,
|
||||
@@ -110,6 +113,7 @@ When used as a return annotation, `PaginatedResponse[T]` automatically expands t
|
||||
from fastapi_toolsets.crud import PaginationType
|
||||
from fastapi_toolsets.schemas import PaginatedResponse
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(
|
||||
pagination_type: PaginationType = PaginationType.OFFSET,
|
||||
|
||||
@@ -6,7 +6,11 @@ You can import the main symbols from `fastapi_toolsets.crud`:
|
||||
|
||||
```python
|
||||
from fastapi_toolsets.crud import CrudFactory, AsyncCrud
|
||||
from fastapi_toolsets.crud.search import SearchConfig, get_searchable_fields, build_search_filters
|
||||
from fastapi_toolsets.crud.search import (
|
||||
SearchConfig,
|
||||
get_searchable_fields,
|
||||
build_search_filters,
|
||||
)
|
||||
```
|
||||
|
||||
## ::: fastapi_toolsets.crud.factory.AsyncCrud
|
||||
|
||||
+14
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "fastapi-toolsets"
|
||||
version = "5.0.0b2"
|
||||
version = "5.1.0"
|
||||
description = "Production-ready utilities for FastAPI applications"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -91,9 +91,21 @@ docs-src = [
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.10,<0.12.0"]
|
||||
requires = ["uv_build>=0.10,<0.13.0"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[tool.ruff.format]
|
||||
exclude = ["*.md"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = ["E712"]
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
extend-immutable-calls = ["fastapi.Depends"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = ["RUF012", "RUF059", "SIM117", "DTZ001", "S110", "BLE001"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = [
|
||||
|
||||
@@ -24,4 +24,4 @@ Example usage:
|
||||
return Response(data={"user": user.username}, message="Success")
|
||||
"""
|
||||
|
||||
__version__ = "5.0.0b2"
|
||||
__version__ = "5.1.0"
|
||||
|
||||
@@ -22,7 +22,6 @@ __all__ = [
|
||||
"AsyncCrud",
|
||||
"CrudFactory",
|
||||
"FacetFieldType",
|
||||
"get_searchable_fields",
|
||||
"InvalidFacetFilterError",
|
||||
"InvalidSearchColumnError",
|
||||
"JoinType",
|
||||
@@ -34,4 +33,5 @@ __all__ = [
|
||||
"SearchConfig",
|
||||
"SearchFieldType",
|
||||
"UnsupportedFacetTypeError",
|
||||
"get_searchable_fields",
|
||||
]
|
||||
|
||||
@@ -44,6 +44,7 @@ from ..types import (
|
||||
)
|
||||
from .search import (
|
||||
SearchConfig,
|
||||
apply_search_joins,
|
||||
build_facets,
|
||||
build_filter_by,
|
||||
build_search_filters,
|
||||
@@ -51,7 +52,6 @@ from .search import (
|
||||
search_field_keys,
|
||||
)
|
||||
|
||||
|
||||
_ForUpdateMode: TypeAlias = bool | Literal["nowait", "skip_locked"]
|
||||
|
||||
|
||||
@@ -128,17 +128,6 @@ def _apply_joins(q: Any, joins: JoinType | None, outer_join: bool) -> Any:
|
||||
return q
|
||||
|
||||
|
||||
def _apply_search_joins(q: Any, search_joins: list[Any]) -> Any:
|
||||
"""Apply relationship-based outer joins (from search/filter_by) to a query."""
|
||||
seen: set[str] = set()
|
||||
for join_rel in search_joins:
|
||||
key = str(join_rel)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
q = q.outerjoin(join_rel)
|
||||
return q
|
||||
|
||||
|
||||
class AsyncCrud(Generic[ModelType]):
|
||||
"""Generic async CRUD operations for SQLAlchemy models.
|
||||
|
||||
@@ -184,17 +173,30 @@ class AsyncCrud(Generic[ModelType]):
|
||||
return cls.default_load_options
|
||||
|
||||
@classmethod
|
||||
async def _reload_with_options(
|
||||
cls: type[Self], session: AsyncSession, instance: ModelType
|
||||
def _capture_pk_values(cls: type[Self], instance: ModelType) -> dict[str, Any]:
|
||||
"""Capture PK values off instance — call before commit expires attributes."""
|
||||
return {
|
||||
cast(str, col.key): getattr(instance, cast(str, col.key))
|
||||
for col in cls.model.__mapper__.primary_key
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def _reload_with_options_by_pk(
|
||||
cls: type[Self], session: AsyncSession, pk_values: dict[str, Any]
|
||||
) -> ModelType:
|
||||
"""Re-query instance by PK with default_load_options applied."""
|
||||
mapper = cls.model.__mapper__
|
||||
"""Re-query by previously captured PK values, with default_load_options applied."""
|
||||
# Only called when cls.default_load_options is set (see call sites).
|
||||
pk_filters = [
|
||||
getattr(cls.model, cast(str, col.key))
|
||||
== getattr(instance, cast(str, col.key))
|
||||
for col in mapper.primary_key
|
||||
getattr(cls.model, key) == value for key, value in pk_values.items()
|
||||
]
|
||||
return await cls.get(session, filters=pk_filters)
|
||||
q = select(cls.model).where(and_(*pk_filters))
|
||||
q = q.execution_options(populate_existing=True)
|
||||
q = q.options(*cast(Sequence[ExecutableOption], cls.default_load_options))
|
||||
result = await session.execute(q)
|
||||
item = result.unique().scalar_one_or_none()
|
||||
if item is None: # pragma: no cover — row was just flushed in this transaction
|
||||
raise NotFoundError()
|
||||
return cast(ModelType, item)
|
||||
|
||||
@classmethod
|
||||
async def _resolve_m2m(
|
||||
@@ -265,12 +267,12 @@ class AsyncCrud(Generic[ModelType]):
|
||||
cls: type[Self],
|
||||
filter_by: dict[str, Any] | BaseModel | None,
|
||||
facet_fields: Sequence[FacetFieldType] | None,
|
||||
) -> tuple[list[Any], list[Any]]:
|
||||
"""Normalize filter_by and return (filters, joins) to apply to the query."""
|
||||
) -> tuple[dict[str, Any], list[Any]]:
|
||||
"""Normalize filter_by and return ({facet_key: filter}, joins) to apply to the query."""
|
||||
if isinstance(filter_by, BaseModel):
|
||||
filter_by = filter_by.model_dump(exclude_none=True)
|
||||
if not filter_by:
|
||||
return [], []
|
||||
return {}, []
|
||||
resolved = cls._resolve_facet_fields(facet_fields)
|
||||
return build_filter_by(filter_by, resolved or [])
|
||||
|
||||
@@ -281,8 +283,13 @@ class AsyncCrud(Generic[ModelType]):
|
||||
facet_fields: Sequence[FacetFieldType] | None,
|
||||
filters: list[Any],
|
||||
search_joins: list[Any],
|
||||
*,
|
||||
include_facets: bool = True,
|
||||
own_filters: dict[str, Any] | None = None,
|
||||
) -> dict[str, list[Any]] | None:
|
||||
"""Build facet filter_attributes, or return None if no facet fields configured."""
|
||||
"""Build facet filter_attributes, or None if disabled/no facet fields configured."""
|
||||
if not include_facets:
|
||||
return None
|
||||
resolved = cls._resolve_facet_fields(facet_fields)
|
||||
if not resolved:
|
||||
return None
|
||||
@@ -292,6 +299,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
resolved,
|
||||
base_filters=filters,
|
||||
base_joins=search_joins,
|
||||
own_filters=own_filters,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -475,6 +483,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
default_page_size: int = 20,
|
||||
max_page_size: int = 100,
|
||||
include_total: bool = True,
|
||||
include_facets: bool = True,
|
||||
search: bool = True,
|
||||
filter: bool = True,
|
||||
order: bool = True,
|
||||
@@ -490,6 +499,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
default_page_size: Default ``items_per_page`` value.
|
||||
max_page_size: Maximum ``items_per_page`` value.
|
||||
include_total: Whether to include total count (not a query param).
|
||||
include_facets: Whether to run facet queries (not a query param).
|
||||
search: Enable search query parameters.
|
||||
filter: Enable facet filter query parameters.
|
||||
order: Enable order query parameters.
|
||||
@@ -519,7 +529,10 @@ class AsyncCrud(Generic[ModelType]):
|
||||
]
|
||||
return cls._build_paginate_params(
|
||||
pagination_params=pagination_params,
|
||||
pagination_fixed={"include_total": include_total},
|
||||
pagination_fixed={
|
||||
"include_total": include_total,
|
||||
"include_facets": include_facets,
|
||||
},
|
||||
dep_name=f"{cls.model.__name__}OffsetPaginateParams",
|
||||
search=search,
|
||||
filter=filter,
|
||||
@@ -537,6 +550,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
*,
|
||||
default_page_size: int = 20,
|
||||
max_page_size: int = 100,
|
||||
include_facets: bool = True,
|
||||
search: bool = True,
|
||||
filter: bool = True,
|
||||
order: bool = True,
|
||||
@@ -551,6 +565,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
Args:
|
||||
default_page_size: Default ``items_per_page`` value.
|
||||
max_page_size: Maximum ``items_per_page`` value.
|
||||
include_facets: Whether to run facet queries (not a query param).
|
||||
search: Enable search query parameters.
|
||||
filter: Enable facet filter query parameters.
|
||||
order: Enable order query parameters.
|
||||
@@ -582,7 +597,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
]
|
||||
return cls._build_paginate_params(
|
||||
pagination_params=pagination_params,
|
||||
pagination_fixed={},
|
||||
pagination_fixed={"include_facets": include_facets},
|
||||
dep_name=f"{cls.model.__name__}CursorPaginateParams",
|
||||
search=search,
|
||||
filter=filter,
|
||||
@@ -602,6 +617,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
max_page_size: int = 100,
|
||||
default_pagination_type: PaginationType = PaginationType.OFFSET,
|
||||
include_total: bool = True,
|
||||
include_facets: bool = True,
|
||||
search: bool = True,
|
||||
filter: bool = True,
|
||||
order: bool = True,
|
||||
@@ -618,6 +634,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
max_page_size: Maximum ``items_per_page`` value.
|
||||
default_pagination_type: Default pagination strategy.
|
||||
include_total: Whether to include total count (not a query param).
|
||||
include_facets: Whether to run facet queries (not a query param).
|
||||
search: Enable search query parameters.
|
||||
filter: Enable facet filter query parameters.
|
||||
order: Enable order query parameters.
|
||||
@@ -666,7 +683,10 @@ class AsyncCrud(Generic[ModelType]):
|
||||
]
|
||||
return cls._build_paginate_params(
|
||||
pagination_params=pagination_params,
|
||||
pagination_fixed={"include_total": include_total},
|
||||
pagination_fixed={
|
||||
"include_total": include_total,
|
||||
"include_facets": include_facets,
|
||||
},
|
||||
dep_name=f"{cls.model.__name__}PaginateParams",
|
||||
search=search,
|
||||
filter=filter,
|
||||
@@ -730,9 +750,14 @@ class AsyncCrud(Generic[ModelType]):
|
||||
setattr(db_model, rel_attr, related_instances)
|
||||
|
||||
session.add(db_model)
|
||||
await session.refresh(db_model)
|
||||
pk_values: dict[str, Any] | None = None
|
||||
if cls.default_load_options:
|
||||
db_model = await cls._reload_with_options(session, db_model)
|
||||
await session.flush()
|
||||
pk_values = cls._capture_pk_values(db_model)
|
||||
if pk_values is not None:
|
||||
db_model = await cls._reload_with_options_by_pk(session, pk_values)
|
||||
else:
|
||||
await session.refresh(db_model)
|
||||
result = cast(ModelType, db_model)
|
||||
if schema:
|
||||
return Response(data=schema.model_validate(result))
|
||||
@@ -1097,9 +1122,15 @@ class AsyncCrud(Generic[ModelType]):
|
||||
m2m_resolved = await cls._resolve_m2m(session, obj, only_set=True)
|
||||
for rel_attr, related_instances in m2m_resolved.items():
|
||||
setattr(db_model, rel_attr, related_instances)
|
||||
await session.refresh(db_model)
|
||||
|
||||
pk_values: dict[str, Any] | None = None
|
||||
if cls.default_load_options:
|
||||
db_model = await cls._reload_with_options(session, db_model)
|
||||
await session.flush()
|
||||
pk_values = cls._capture_pk_values(db_model)
|
||||
if pk_values is not None:
|
||||
db_model = await cls._reload_with_options_by_pk(session, pk_values)
|
||||
else:
|
||||
await session.refresh(db_model)
|
||||
if schema:
|
||||
return Response(data=schema.model_validate(db_model))
|
||||
return db_model
|
||||
@@ -1271,6 +1302,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column: str | None = None,
|
||||
order_fields: Sequence[OrderFieldType] | None = None,
|
||||
facet_fields: Sequence[FacetFieldType] | None = None,
|
||||
include_facets: bool = True,
|
||||
filter_by: dict[str, Any] | BaseModel | None = None,
|
||||
schema: type[BaseModel],
|
||||
) -> OffsetPaginatedResponse[Any]:
|
||||
@@ -1292,6 +1324,9 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column: Restrict search to a single column key.
|
||||
order_fields: Fields allowed for sorting (overrides class default).
|
||||
facet_fields: Columns to compute distinct values for (overrides class default)
|
||||
include_facets: When ``False``, skip facet queries entirely;
|
||||
``filter_attributes`` will be ``None``. Useful on pages 2..N
|
||||
where the facet counts were already fetched on page 1.
|
||||
filter_by: Dict of {column_key: value} to filter by declared facet fields.
|
||||
Keys must match the column.key of a facet field. Scalar → equality,
|
||||
list → IN clause. Raises InvalidFacetFilterError for unknown keys.
|
||||
@@ -1304,7 +1339,6 @@ class AsyncCrud(Generic[ModelType]):
|
||||
offset = (page - 1) * items_per_page
|
||||
|
||||
fb_filters, search_joins = cls._prepare_filter_by(filter_by, facet_fields)
|
||||
filters.extend(fb_filters)
|
||||
|
||||
# Build search filters
|
||||
if search:
|
||||
@@ -1318,6 +1352,11 @@ class AsyncCrud(Generic[ModelType]):
|
||||
filters.extend(search_filters)
|
||||
search_joins.extend(new_search_joins)
|
||||
|
||||
# Facets combine these with each facet's own filter individually, so
|
||||
# fb_filters is applied to the query below but excluded here.
|
||||
facet_base_filters = list(filters)
|
||||
filters.extend(fb_filters.values())
|
||||
|
||||
# Build query with joins
|
||||
q = select(cls.model)
|
||||
|
||||
@@ -1325,11 +1364,11 @@ class AsyncCrud(Generic[ModelType]):
|
||||
q = _apply_joins(q, joins, outer_join)
|
||||
|
||||
# Apply search joins (always outer joins for search)
|
||||
q = _apply_search_joins(q, search_joins)
|
||||
q = apply_search_joins(q, search_joins)
|
||||
|
||||
# Apply order joins (relation joins required for order_by field)
|
||||
if order_joins:
|
||||
q = _apply_search_joins(q, order_joins)
|
||||
q = apply_search_joins(q, order_joins)
|
||||
|
||||
if filters:
|
||||
q = q.where(and_(*filters))
|
||||
@@ -1352,7 +1391,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
count_q = _apply_joins(count_q, joins, outer_join)
|
||||
|
||||
# Apply search joins to count query
|
||||
count_q = _apply_search_joins(count_q, search_joins)
|
||||
count_q = apply_search_joins(count_q, search_joins)
|
||||
|
||||
if filters:
|
||||
count_q = count_q.where(and_(*filters))
|
||||
@@ -1372,7 +1411,12 @@ class AsyncCrud(Generic[ModelType]):
|
||||
items: list[Any] = [schema.model_validate(item) for item in raw_items]
|
||||
|
||||
filter_attributes = await cls._build_filter_attributes(
|
||||
session, facet_fields, filters, search_joins
|
||||
session,
|
||||
facet_fields,
|
||||
facet_base_filters,
|
||||
search_joins,
|
||||
include_facets=include_facets,
|
||||
own_filters=fb_filters,
|
||||
)
|
||||
search_columns = cls._resolve_search_columns(search_fields)
|
||||
order_columns = cls._resolve_order_columns(order_fields)
|
||||
@@ -1408,6 +1452,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column: str | None = None,
|
||||
order_fields: Sequence[OrderFieldType] | None = None,
|
||||
facet_fields: Sequence[FacetFieldType] | None = None,
|
||||
include_facets: bool = True,
|
||||
filter_by: dict[str, Any] | BaseModel | None = None,
|
||||
schema: type[BaseModel],
|
||||
) -> CursorPaginatedResponse[Any]:
|
||||
@@ -1430,6 +1475,8 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column: Restrict search to a single column key.
|
||||
order_fields: Fields allowed for sorting (overrides class default).
|
||||
facet_fields: Columns to compute distinct values for (overrides class default).
|
||||
include_facets: When ``False``, skip facet queries entirely;
|
||||
``filter_attributes`` will be ``None``.
|
||||
filter_by: Dict of {column_key: value} to filter by declared facet fields.
|
||||
Keys must match the column.key of a facet field. Scalar → equality,
|
||||
list → IN clause. Raises InvalidFacetFilterError for unknown keys.
|
||||
@@ -1441,7 +1488,6 @@ class AsyncCrud(Generic[ModelType]):
|
||||
filters = list(filters) if filters else []
|
||||
|
||||
fb_filters, search_joins = cls._prepare_filter_by(filter_by, facet_fields)
|
||||
filters.extend(fb_filters)
|
||||
|
||||
if cls.cursor_column is None:
|
||||
raise ValueError(
|
||||
@@ -1473,6 +1519,11 @@ class AsyncCrud(Generic[ModelType]):
|
||||
filters.extend(search_filters)
|
||||
search_joins.extend(new_search_joins)
|
||||
|
||||
# Facets combine these with each facet's own filter individually, so
|
||||
# fb_filters is applied to the query below but excluded here.
|
||||
facet_base_filters = list(filters)
|
||||
filters.extend(fb_filters.values())
|
||||
|
||||
# Build query
|
||||
q = select(cls.model)
|
||||
|
||||
@@ -1480,11 +1531,11 @@ class AsyncCrud(Generic[ModelType]):
|
||||
q = _apply_joins(q, joins, outer_join)
|
||||
|
||||
# Apply search joins (always outer joins)
|
||||
q = _apply_search_joins(q, search_joins)
|
||||
q = apply_search_joins(q, search_joins)
|
||||
|
||||
# Apply order joins (relation joins required for order_by field)
|
||||
if order_joins:
|
||||
q = _apply_search_joins(q, order_joins)
|
||||
q = apply_search_joins(q, order_joins)
|
||||
|
||||
if filters:
|
||||
q = q.where(and_(*filters))
|
||||
@@ -1529,11 +1580,10 @@ class AsyncCrud(Generic[ModelType]):
|
||||
|
||||
# prev_cursor: points before the first item in ascending order
|
||||
prev_cursor: str | None = None
|
||||
if direction is _CursorDirection.NEXT and cursor is not None and items_page:
|
||||
prev_cursor = _encode_cursor(
|
||||
getattr(items_page[0], cursor_col_name), direction=_CursorDirection.PREV
|
||||
)
|
||||
elif direction is _CursorDirection.PREV and has_more and items_page:
|
||||
if items_page and (
|
||||
(direction is _CursorDirection.NEXT and cursor is not None)
|
||||
or (direction is _CursorDirection.PREV and has_more)
|
||||
):
|
||||
prev_cursor = _encode_cursor(
|
||||
getattr(items_page[0], cursor_col_name), direction=_CursorDirection.PREV
|
||||
)
|
||||
@@ -1541,7 +1591,12 @@ class AsyncCrud(Generic[ModelType]):
|
||||
items: list[Any] = [schema.model_validate(item) for item in items_page]
|
||||
|
||||
filter_attributes = await cls._build_filter_attributes(
|
||||
session, facet_fields, filters, search_joins
|
||||
session,
|
||||
facet_fields,
|
||||
facet_base_filters,
|
||||
search_joins,
|
||||
include_facets=include_facets,
|
||||
own_filters=fb_filters,
|
||||
)
|
||||
search_columns = cls._resolve_search_columns(search_fields)
|
||||
order_columns = cls._resolve_order_columns(order_fields)
|
||||
@@ -1581,6 +1636,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column: str | None = ...,
|
||||
order_fields: Sequence[OrderFieldType] | None = ...,
|
||||
facet_fields: Sequence[FacetFieldType] | None = ...,
|
||||
include_facets: bool = ...,
|
||||
filter_by: dict[str, Any] | BaseModel | None = ...,
|
||||
schema: type[BaseModel],
|
||||
) -> OffsetPaginatedResponse[Any]: ...
|
||||
@@ -1607,6 +1663,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column: str | None = ...,
|
||||
order_fields: Sequence[OrderFieldType] | None = ...,
|
||||
facet_fields: Sequence[FacetFieldType] | None = ...,
|
||||
include_facets: bool = ...,
|
||||
filter_by: dict[str, Any] | BaseModel | None = ...,
|
||||
schema: type[BaseModel],
|
||||
) -> CursorPaginatedResponse[Any]: ...
|
||||
@@ -1632,6 +1689,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column: str | None = None,
|
||||
order_fields: Sequence[OrderFieldType] | None = None,
|
||||
facet_fields: Sequence[FacetFieldType] | None = None,
|
||||
include_facets: bool = True,
|
||||
filter_by: dict[str, Any] | BaseModel | None = None,
|
||||
schema: type[BaseModel],
|
||||
) -> OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any]:
|
||||
@@ -1662,6 +1720,8 @@ class AsyncCrud(Generic[ModelType]):
|
||||
order_fields: Fields allowed for sorting (overrides class default).
|
||||
facet_fields: Columns to compute distinct values for (overrides
|
||||
class default).
|
||||
include_facets: When ``False``, skip facet queries entirely;
|
||||
``filter_attributes`` will be ``None``.
|
||||
filter_by: Dict of ``{column_key: value}`` to filter by declared
|
||||
facet fields. Keys must match the ``column.key`` of a facet
|
||||
field. Scalar → equality, list → IN clause. Raises
|
||||
@@ -1692,6 +1752,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column=search_column,
|
||||
order_fields=order_fields,
|
||||
facet_fields=facet_fields,
|
||||
include_facets=include_facets,
|
||||
filter_by=filter_by,
|
||||
schema=schema,
|
||||
)
|
||||
@@ -1714,6 +1775,7 @@ class AsyncCrud(Generic[ModelType]):
|
||||
search_column=search_column,
|
||||
order_fields=order_fields,
|
||||
facet_fields=facet_fields,
|
||||
include_facets=include_facets,
|
||||
filter_by=filter_by,
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Search utilities for AsyncCrud."""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from sqlalchemy import String, and_, func, or_, select
|
||||
from sqlalchemy import String, and_, distinct, func, or_, select
|
||||
from sqlalchemy.dialects.postgresql import aggregate_order_by
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
@@ -159,8 +159,11 @@ def build_search_filters(
|
||||
else:
|
||||
column = field
|
||||
|
||||
# Build the filter (cast to String for non-text columns)
|
||||
column_as_string = column.cast(String)
|
||||
# Build the filter (cast to String only when needed, to preserve
|
||||
# pg_trgm GIN index usability on already-String columns)
|
||||
column_as_string = (
|
||||
column if isinstance(column.type, String) else column.cast(String)
|
||||
)
|
||||
if config.case_sensitive:
|
||||
filters.append(column_as_string.like(f"%{query}%"))
|
||||
else:
|
||||
@@ -181,6 +184,21 @@ def search_field_keys(fields: Sequence[SearchFieldType]) -> list[str]:
|
||||
return facet_keys(fields)
|
||||
|
||||
|
||||
def apply_search_joins(q: Any, joins: Sequence[Any]) -> Any:
|
||||
"""Apply relationship-based outer joins (from search/filter_by/facets) to a query.
|
||||
|
||||
Deduplicates by relationship identity so a join used by several fields
|
||||
(e.g. search + a facet on the same relation) is only applied once.
|
||||
"""
|
||||
seen: set[str] = set()
|
||||
for rel in joins:
|
||||
rel_key = str(rel)
|
||||
if rel_key not in seen:
|
||||
seen.add(rel_key)
|
||||
q = q.outerjoin(rel)
|
||||
return q
|
||||
|
||||
|
||||
def facet_keys(facet_fields: Sequence[FacetFieldType]) -> list[str]:
|
||||
"""Return a key for each facet field.
|
||||
|
||||
@@ -207,6 +225,7 @@ async def build_facets(
|
||||
*,
|
||||
base_filters: "list[ColumnElement[bool]] | None" = None,
|
||||
base_joins: list[InstrumentedAttribute[Any]] | None = None,
|
||||
own_filters: "dict[str, ColumnElement[bool]] | None" = None,
|
||||
) -> dict[str, list[Any]]:
|
||||
"""Return distinct values for each facet field, respecting current filters.
|
||||
|
||||
@@ -216,15 +235,24 @@ async def build_facets(
|
||||
facet_fields: Columns or relationship tuples to facet on
|
||||
base_filters: Filter conditions already applied to the main query (search + caller filters)
|
||||
base_joins: Relationship joins already applied to the main query
|
||||
own_filters: Map of facet key -> the ``filter_by`` condition for that
|
||||
same key (if any). Excluded from that facet's own subquery so
|
||||
filtering on a facet doesn't collapse its own value list down to
|
||||
just the filtered value.
|
||||
|
||||
Returns:
|
||||
Dict mapping column key to sorted list of distinct non-None values
|
||||
"""
|
||||
existing_join_keys: set[str] = {str(j) for j in (base_joins or [])}
|
||||
if not facet_fields:
|
||||
return {}
|
||||
|
||||
keys = facet_keys(facet_fields)
|
||||
own_filters = own_filters or {}
|
||||
|
||||
async def _query_facet(field: FacetFieldType, key: str) -> tuple[str, list[Any]]:
|
||||
scalars: list[Any] = []
|
||||
enum_classes: dict[str, Any] = {}
|
||||
|
||||
for field, key in zip(facet_fields, keys):
|
||||
if isinstance(field, tuple):
|
||||
# Relationship chain: (User.role, Role.name) — last element is the column
|
||||
rels = field[:-1]
|
||||
@@ -235,51 +263,48 @@ async def build_facets(
|
||||
|
||||
col_type = column.property.columns[0].type
|
||||
is_array = isinstance(col_type, ARRAY)
|
||||
enum_classes[key] = getattr(col_type, "enum_class", None)
|
||||
|
||||
if is_array:
|
||||
unnested = func.unnest(column).label(column.key)
|
||||
q = select(unnested).select_from(model).distinct()
|
||||
else:
|
||||
q = select(column).select_from(model).distinct()
|
||||
|
||||
# Apply base joins (deduplicated) — needed here independently
|
||||
seen_joins: set[str] = set()
|
||||
for rel in base_joins or []:
|
||||
rel_key = str(rel)
|
||||
if rel_key not in seen_joins:
|
||||
seen_joins.add(rel_key)
|
||||
q = q.outerjoin(rel)
|
||||
|
||||
# Add any extra joins required by this facet field that aren't already applied
|
||||
for rel in rels:
|
||||
rel_key = str(rel)
|
||||
if rel_key not in existing_join_keys and rel_key not in seen_joins:
|
||||
seen_joins.add(rel_key)
|
||||
q = q.outerjoin(rel)
|
||||
|
||||
if base_filters:
|
||||
q = q.where(and_(*base_filters))
|
||||
|
||||
if is_array:
|
||||
q = q.order_by(unnested)
|
||||
else:
|
||||
q = q.order_by(column)
|
||||
result = await session.execute(q)
|
||||
col_type = column.property.columns[0].type
|
||||
enum_class = getattr(col_type, "enum_class", None)
|
||||
values = [
|
||||
row[0].name
|
||||
if (enum_class is not None and isinstance(row[0], enum_class))
|
||||
else row[0]
|
||||
for row in result.all()
|
||||
if row[0] is not None
|
||||
filters = [
|
||||
*(base_filters or []),
|
||||
*(f for k, f in own_filters.items() if k != key),
|
||||
]
|
||||
return key, values
|
||||
joins = [*(base_joins or []), *rels]
|
||||
|
||||
pairs = await asyncio.gather(
|
||||
*[_query_facet(f, k) for f, k in zip(facet_fields, keys)]
|
||||
if is_array:
|
||||
unnested = apply_search_joins(
|
||||
select(func.unnest(column).label("v")).select_from(model), joins
|
||||
)
|
||||
return dict(pairs)
|
||||
if filters:
|
||||
unnested = unnested.where(and_(*filters))
|
||||
unnested_sq = unnested.subquery()
|
||||
v = unnested_sq.c.v
|
||||
agg = (
|
||||
select(func.array_agg(aggregate_order_by(distinct(v), v)))
|
||||
.select_from(unnested_sq)
|
||||
.where(v.isnot(None))
|
||||
)
|
||||
else:
|
||||
agg = apply_search_joins(
|
||||
select(
|
||||
func.array_agg(aggregate_order_by(distinct(column), column))
|
||||
).select_from(model),
|
||||
joins,
|
||||
)
|
||||
agg = agg.where(and_(*filters, column.isnot(None)))
|
||||
|
||||
scalars.append(agg.scalar_subquery().label(key))
|
||||
|
||||
row = (await session.execute(select(*scalars))).one()
|
||||
|
||||
facets: dict[str, list[Any]] = {}
|
||||
for key, values in zip(keys, row):
|
||||
enum_class = enum_classes[key]
|
||||
facets[key] = [
|
||||
v.name if (enum_class is not None and isinstance(v, enum_class)) else v
|
||||
for v in (values or [])
|
||||
]
|
||||
return facets
|
||||
|
||||
|
||||
_EQUALITY_TYPES = (String, Integer, Numeric, Date, DateTime, Time, Enum, Uuid)
|
||||
@@ -301,7 +326,7 @@ def _coerce_bool(value: Any) -> bool:
|
||||
def build_filter_by(
|
||||
filter_by: dict[str, Any],
|
||||
facet_fields: Sequence[FacetFieldType],
|
||||
) -> tuple["list[ColumnElement[bool]]", list[InstrumentedAttribute[Any]]]:
|
||||
) -> tuple["dict[str, ColumnElement[bool]]", list[InstrumentedAttribute[Any]]]:
|
||||
"""Translate a {column_key: value} dict into SQLAlchemy filter conditions.
|
||||
|
||||
Args:
|
||||
@@ -309,7 +334,9 @@ def build_filter_by(
|
||||
facet_fields: Declared facet fields to validate keys against
|
||||
|
||||
Returns:
|
||||
Tuple of (filter_conditions, joins_needed)
|
||||
Tuple of ({facet_key: filter_condition}, joins_needed). One filter
|
||||
condition per key, so callers can identify (and exclude) a facet's
|
||||
own filter when computing that facet's distinct values.
|
||||
|
||||
Raises:
|
||||
InvalidFacetFilterError: If a key in filter_by is not a declared facet field
|
||||
@@ -327,7 +354,7 @@ def build_filter_by(
|
||||
index[key] = (column, rels)
|
||||
|
||||
valid_keys = set(index)
|
||||
filters: list[ColumnElement[bool]] = []
|
||||
filters: dict[str, ColumnElement[bool]] = {}
|
||||
joins: list[InstrumentedAttribute[Any]] = []
|
||||
added_join_keys: set[str] = set()
|
||||
|
||||
@@ -347,37 +374,37 @@ def build_filter_by(
|
||||
if isinstance(col_type, Boolean):
|
||||
coerce = _coerce_bool
|
||||
if isinstance(value, list):
|
||||
filters.append(column.in_([coerce(v) for v in value]))
|
||||
filters[key] = column.in_([coerce(v) for v in value])
|
||||
else:
|
||||
filters.append(column == coerce(value))
|
||||
filters[key] = column == coerce(value)
|
||||
elif isinstance(col_type, ARRAY):
|
||||
if isinstance(value, list):
|
||||
filters.append(column.overlap(value))
|
||||
filters[key] = column.overlap(value)
|
||||
else:
|
||||
filters.append(column.any(value))
|
||||
filters[key] = column.any(value)
|
||||
elif isinstance(col_type, Enum):
|
||||
enum_class = col_type.enum_class
|
||||
if enum_class is not None:
|
||||
|
||||
def _coerce_enum(v: Any) -> Any:
|
||||
def _coerce_enum(v: Any, enum_class: Any = enum_class) -> Any:
|
||||
if isinstance(v, enum_class):
|
||||
return v
|
||||
return enum_class[v] # lookup by name: "PENDING", "RED"
|
||||
|
||||
if isinstance(value, list):
|
||||
filters.append(column.in_([_coerce_enum(v) for v in value]))
|
||||
filters[key] = column.in_([_coerce_enum(v) for v in value])
|
||||
else:
|
||||
filters.append(column == _coerce_enum(value))
|
||||
filters[key] = column == _coerce_enum(value)
|
||||
else: # pragma: no cover
|
||||
if isinstance(value, list):
|
||||
filters.append(column.in_(value))
|
||||
filters[key] = column.in_(value)
|
||||
else:
|
||||
filters.append(column == value)
|
||||
filters[key] = column == value
|
||||
elif isinstance(col_type, _EQUALITY_TYPES):
|
||||
if isinstance(value, list):
|
||||
filters.append(column.in_(value))
|
||||
filters[key] = column.in_(value)
|
||||
else:
|
||||
filters.append(column == value)
|
||||
filters[key] = column == value
|
||||
else:
|
||||
raise UnsupportedFacetTypeError(key, type(col_type).__name__)
|
||||
|
||||
|
||||
@@ -212,8 +212,7 @@ class Database:
|
||||
|
||||
@asynccontextmanager
|
||||
async def _composed(app_: Any) -> AsyncGenerator[None, None]:
|
||||
async with self.lifespan(app_):
|
||||
async with inner_lifespan(app_):
|
||||
async with self.lifespan(app_), inner_lifespan(app_):
|
||||
yield
|
||||
|
||||
app.router.lifespan_context = _composed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Many-to-Many association-table helpers (direct, without loading collections)."""
|
||||
|
||||
from typing import Any, TypeVar, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from sqlalchemy import ColumnElement, Table, delete, tuple_
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
@@ -8,8 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import DeclarativeBase, QueryableAttribute
|
||||
from sqlalchemy.orm.relationships import RelationshipProperty
|
||||
|
||||
_M = TypeVar("_M", bound=DeclarativeBase)
|
||||
|
||||
|
||||
def _m2m_prop(rel_attr: QueryableAttribute) -> tuple[RelationshipProperty, Table]: # type: ignore[type-arg]
|
||||
"""Return the validated M2M RelationshipProperty and its secondary table.
|
||||
|
||||
@@ -60,7 +60,7 @@ def PathDependency(
|
||||
name = (
|
||||
param_name
|
||||
if param_name is not None
|
||||
else "{}_{}".format(model.__name__.lower(), field.key)
|
||||
else f"{model.__name__.lower()}_{field.key}"
|
||||
)
|
||||
python_type = field.type.python_type
|
||||
|
||||
@@ -70,10 +70,7 @@ def PathDependency(
|
||||
value = kwargs[name]
|
||||
return await crud.get(session, filters=[field == value])
|
||||
|
||||
setattr(
|
||||
dependency,
|
||||
"__signature__",
|
||||
inspect.Signature(
|
||||
dependency.__signature__ = inspect.Signature( # ty:ignore[unresolved-attribute]
|
||||
parameters=[
|
||||
inspect.Parameter(
|
||||
name, inspect.Parameter.KEYWORD_ONLY, annotation=python_type
|
||||
@@ -85,7 +82,6 @@ def PathDependency(
|
||||
default=Depends(session_callable),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
|
||||
@@ -134,10 +130,7 @@ def BodyDependency(
|
||||
value = kwargs[body_field]
|
||||
return await crud.get(session, filters=[field == value])
|
||||
|
||||
setattr(
|
||||
dependency,
|
||||
"__signature__",
|
||||
inspect.Signature(
|
||||
dependency.__signature__ = inspect.Signature( # ty:ignore[unresolved-attribute]
|
||||
parameters=[
|
||||
inspect.Parameter(
|
||||
body_field, inspect.Parameter.KEYWORD_ONLY, annotation=python_type
|
||||
@@ -149,7 +142,6 @@ def BodyDependency(
|
||||
default=Depends(session_callable),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
|
||||
|
||||
@@ -23,8 +23,6 @@ __all__ = [
|
||||
"ApiException",
|
||||
"ConflictError",
|
||||
"ForbiddenError",
|
||||
"generate_error_responses",
|
||||
"init_exceptions_handlers",
|
||||
"InvalidFacetFilterError",
|
||||
"InvalidOrderFieldError",
|
||||
"InvalidSearchColumnError",
|
||||
@@ -34,4 +32,6 @@ __all__ = [
|
||||
"PoolExhaustedError",
|
||||
"UnauthorizedError",
|
||||
"UnsupportedFacetTypeError",
|
||||
"generate_error_responses",
|
||||
"init_exceptions_handlers",
|
||||
]
|
||||
|
||||
@@ -146,8 +146,7 @@ def _patched_openapi(
|
||||
|
||||
for path_data in openapi_schema.get("paths", {}).values():
|
||||
for operation in path_data.values():
|
||||
if isinstance(operation, dict) and "responses" in operation:
|
||||
if "422" in operation["responses"]:
|
||||
if isinstance(operation, dict) and "422" in operation.get("responses", {}):
|
||||
operation["responses"]["422"] = {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
from .columns import (
|
||||
CreatedAtMixin,
|
||||
TimestampMixin,
|
||||
UpdatedAtMixin,
|
||||
UUIDMixin,
|
||||
UUIDv7Mixin,
|
||||
UpdatedAtMixin,
|
||||
)
|
||||
from .watched import EventSession, ModelEvent, listens_for
|
||||
|
||||
__all__ = [
|
||||
"CreatedAtMixin",
|
||||
"EventSession",
|
||||
"ModelEvent",
|
||||
"TimestampMixin",
|
||||
"UUIDMixin",
|
||||
"UUIDv7Mixin",
|
||||
"CreatedAtMixin",
|
||||
"UpdatedAtMixin",
|
||||
"TimestampMixin",
|
||||
"listens_for",
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy import event, select, tuple_
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm.attributes import set_committed_value as _sa_set_committed_value
|
||||
@@ -29,14 +29,11 @@ _SESSION_DELETES = "_ft_deletes"
|
||||
_SESSION_UPDATES = "_ft_updates"
|
||||
_DEFERRED_STRATEGY_KEY = (("deferred", True), ("instrument", True))
|
||||
_EVENT_HANDLERS: dict[tuple[type, ModelEvent], list[Callable[..., Any]]] = {}
|
||||
_WATCHED_MODELS: set[type] = set()
|
||||
_WATCHED_CACHE: dict[type, bool] = {}
|
||||
_HANDLER_CACHE: dict[tuple[type, ModelEvent], list[Callable[..., Any]]] = {}
|
||||
|
||||
|
||||
def _invalidate_caches() -> None:
|
||||
"""Clear lookup caches after handler registration."""
|
||||
_WATCHED_CACHE.clear()
|
||||
_HANDLER_CACHE.clear()
|
||||
|
||||
|
||||
@@ -56,24 +53,12 @@ def listens_for(
|
||||
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
||||
for ev in evs:
|
||||
_EVENT_HANDLERS.setdefault((model_class, ev), []).append(fn)
|
||||
_WATCHED_MODELS.add(model_class)
|
||||
_invalidate_caches()
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _is_watched(obj: Any) -> bool:
|
||||
"""Return True if *obj*'s type (or any ancestor) has registered handlers."""
|
||||
cls = type(obj)
|
||||
try:
|
||||
return _WATCHED_CACHE[cls]
|
||||
except KeyError:
|
||||
result = any(klass in _WATCHED_MODELS for klass in cls.__mro__)
|
||||
_WATCHED_CACHE[cls] = result
|
||||
return result
|
||||
|
||||
|
||||
def _get_handlers(cls: type, ev: ModelEvent) -> list[Callable[..., Any]]:
|
||||
"""Return registered handlers for *cls* and *ev*, walking the MRO."""
|
||||
key = (cls, ev)
|
||||
@@ -144,18 +129,18 @@ def _upsert_changes(
|
||||
def _after_flush(session: Any, flush_context: Any) -> None:
|
||||
# New objects: capture reference. Attributes will be refreshed after commit.
|
||||
for obj in session.new:
|
||||
if _is_watched(obj):
|
||||
if _get_handlers(type(obj), ModelEvent.CREATE):
|
||||
session.info.setdefault(_SESSION_CREATES, []).append(obj)
|
||||
|
||||
# Deleted objects: snapshot now while attributes are still loaded.
|
||||
for obj in session.deleted:
|
||||
if _is_watched(obj):
|
||||
if _get_handlers(type(obj), ModelEvent.DELETE):
|
||||
snapshot = _snapshot_column_attrs(obj)
|
||||
session.info.setdefault(_SESSION_DELETES, []).append((obj, snapshot))
|
||||
|
||||
# Dirty objects: read old/new from SQLAlchemy attribute history.
|
||||
for obj in session.dirty:
|
||||
if not _is_watched(obj):
|
||||
if not _get_handlers(type(obj), ModelEvent.UPDATE):
|
||||
continue
|
||||
|
||||
watched = _get_watched_fields(type(obj))
|
||||
@@ -204,15 +189,24 @@ async def _invoke_callback(
|
||||
await result
|
||||
|
||||
|
||||
async def _reload_if_present(session: AsyncSession, obj: Any, state: Any) -> None:
|
||||
"""Re-populate *obj* from the DB if its row still exists."""
|
||||
await session.get(type(obj), state.key[1], populate_existing=True)
|
||||
async def _batch_reload(
|
||||
session: AsyncSession, model: type, pk_tuples: list[tuple[Any, ...]]
|
||||
) -> None:
|
||||
"""Re-populate all rows of *model* identified by *pk_tuples* in one round trip."""
|
||||
pk_cols = sa_inspect(model, raiseerr=True).primary_key
|
||||
where = (
|
||||
pk_cols[0].in_([pk[0] for pk in pk_tuples])
|
||||
if len(pk_cols) == 1
|
||||
else tuple_(*pk_cols).in_(pk_tuples)
|
||||
)
|
||||
q = select(model).where(where).execution_options(populate_existing=True)
|
||||
await session.execute(q)
|
||||
|
||||
|
||||
class EventSession(AsyncSession):
|
||||
"""AsyncSession subclass that dispatches lifecycle callbacks after commit."""
|
||||
|
||||
async def commit(self) -> None: # noqa: C901
|
||||
async def commit(self) -> None:
|
||||
await super().commit()
|
||||
|
||||
creates: list[Any] = self.info.pop(_SESSION_CREATES, [])
|
||||
@@ -250,15 +244,36 @@ class EventSession(AsyncSession):
|
||||
k: v for k, v in field_changes.items() if k not in create_ids
|
||||
}
|
||||
|
||||
# Dispatch CREATE callbacks.
|
||||
# Resolve reloadable state up front and group PKs by model type so
|
||||
# the post-commit reload is one query per type instead of one
|
||||
# session.get() per object.
|
||||
create_items: list[Any] = []
|
||||
update_items: list[tuple[Any, dict[str, dict[str, Any]]]] = []
|
||||
pk_by_type: dict[type, list[tuple[Any, ...]]] = {}
|
||||
|
||||
for obj in creates:
|
||||
try:
|
||||
state = sa_inspect(obj, raiseerr=False)
|
||||
if (
|
||||
state is None or state.detached or state.transient
|
||||
): # pragma: no cover
|
||||
if state is None or state.detached or state.transient: # pragma: no cover
|
||||
continue
|
||||
await _reload_if_present(self, obj, state)
|
||||
create_items.append(obj)
|
||||
pk_by_type.setdefault(type(obj), []).append(state.key[1])
|
||||
|
||||
for obj, changes in field_changes.values():
|
||||
state = sa_inspect(obj, raiseerr=False)
|
||||
if state is None or state.detached or state.transient: # pragma: no cover
|
||||
continue
|
||||
update_items.append((obj, changes))
|
||||
pk_by_type.setdefault(type(obj), []).append(state.key[1])
|
||||
|
||||
for model, pk_tuples in pk_by_type.items():
|
||||
try:
|
||||
await _batch_reload(self, model, pk_tuples)
|
||||
except Exception as exc:
|
||||
_logger.error(_CALLBACK_ERROR_MSG, exc_info=exc)
|
||||
|
||||
# Dispatch CREATE callbacks.
|
||||
for obj in create_items:
|
||||
try:
|
||||
for handler in _get_handlers(type(obj), ModelEvent.CREATE):
|
||||
await _invoke_callback(handler, obj, ModelEvent.CREATE, None)
|
||||
except Exception as exc:
|
||||
@@ -275,14 +290,8 @@ class EventSession(AsyncSession):
|
||||
_logger.error(_CALLBACK_ERROR_MSG, exc_info=exc)
|
||||
|
||||
# Dispatch UPDATE callbacks.
|
||||
for obj, changes in field_changes.values():
|
||||
for obj, changes in update_items:
|
||||
try:
|
||||
state = sa_inspect(obj, raiseerr=False)
|
||||
if (
|
||||
state is None or state.detached or state.transient
|
||||
): # pragma: no cover
|
||||
continue
|
||||
await _reload_if_present(self, obj, state)
|
||||
for handler in _get_handlers(type(obj), ModelEvent.UPDATE):
|
||||
await _invoke_callback(handler, obj, ModelEvent.UPDATE, changes)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import math
|
||||
from enum import Enum
|
||||
from typing import Annotated, Any, ClassVar, Generic, Literal, TypeVar, Union
|
||||
from typing import Annotated, Any, ClassVar, Generic, Literal, TypeVar
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
||||
|
||||
@@ -10,11 +10,11 @@ from .types import DataT
|
||||
|
||||
__all__ = [
|
||||
"ApiError",
|
||||
"CursorPagination",
|
||||
"CursorPaginatedResponse",
|
||||
"CursorPagination",
|
||||
"ErrorResponse",
|
||||
"OffsetPagination",
|
||||
"OffsetPaginatedResponse",
|
||||
"OffsetPagination",
|
||||
"PaginatedResponse",
|
||||
"PaginationType",
|
||||
"PydanticBase",
|
||||
@@ -174,7 +174,7 @@ class PaginatedResponse(BaseResponse, Generic[DataT]):
|
||||
cached = cls._discriminated_union_cache.get(item)
|
||||
if cached is None:
|
||||
cached = Annotated[
|
||||
Union[CursorPaginatedResponse[item], OffsetPaginatedResponse[item]], # ty:ignore[invalid-type-form]
|
||||
CursorPaginatedResponse[item] | OffsetPaginatedResponse[item], # ty:ignore[invalid-type-form]
|
||||
Field(discriminator="pagination_type"),
|
||||
]
|
||||
cls._discriminated_union_cache[item] = cached
|
||||
|
||||
+29
-7
@@ -1,27 +1,28 @@
|
||||
"""Shared pytest fixtures for fastapi-utils tests."""
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
import os
|
||||
import uuid
|
||||
from enum import Enum
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Column,
|
||||
Date,
|
||||
DateTime,
|
||||
Enum as SAEnum,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
JSON,
|
||||
Numeric,
|
||||
String,
|
||||
Table,
|
||||
Uuid,
|
||||
)
|
||||
from sqlalchemy import (
|
||||
Enum as SAEnum,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import ARRAY
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
@@ -38,8 +39,6 @@ DATABASE_URL = os.getenv(
|
||||
class Base(DeclarativeBase):
|
||||
"""Base class for test models."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Role(Base):
|
||||
"""Test role model."""
|
||||
@@ -476,3 +475,26 @@ async def db_session(engine):
|
||||
# Drop tables after test
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def db_session_expire_on_commit(engine):
|
||||
"""Session with expire_on_commit=True (the SQLAlchemy default).
|
||||
|
||||
Attributes read off an instance after commit are expired and trigger an
|
||||
implicit (sync) refresh under this setting — which fails under asyncio
|
||||
with MissingGreenlet. The other ``db_session`` fixture uses
|
||||
``expire_on_commit=False`` and would not catch that class of bug.
|
||||
"""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=True)
|
||||
session = session_factory()
|
||||
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
@@ -554,7 +554,6 @@ class TestAsyncCommand:
|
||||
@async_command
|
||||
async def async_func() -> None:
|
||||
"""This is a docstring."""
|
||||
pass
|
||||
|
||||
assert async_func.__doc__ == """This is a docstring."""
|
||||
|
||||
|
||||
@@ -417,6 +417,55 @@ class TestDefaultLoadOptionsIntegration:
|
||||
assert updated.role is not None
|
||||
assert updated.role.name == "admin"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_default_load_options_applied_to_create_expire_on_commit(
|
||||
self, db_session_expire_on_commit: AsyncSession
|
||||
):
|
||||
"""create()'s reload uses captured PK values, not an expired instance attribute.
|
||||
|
||||
Regression test for MissingGreenlet: reading a PK off `db_model` after
|
||||
commit under expire_on_commit=True (the SQLAlchemy default) would
|
||||
trigger an implicit sync refresh, which fails under asyncio.
|
||||
"""
|
||||
UserWithDefaultLoad = CrudFactory(
|
||||
User, default_load_options=[selectinload(User.role)]
|
||||
)
|
||||
role = await RoleCrud.create(
|
||||
db_session_expire_on_commit, RoleCreate(name="admin")
|
||||
)
|
||||
user = await UserWithDefaultLoad.create(
|
||||
db_session_expire_on_commit,
|
||||
UserCreate(username="alice", email="alice@test.com", role_id=role.id),
|
||||
)
|
||||
assert user.role is not None
|
||||
assert user.role.name == "admin"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_default_load_options_applied_to_update_expire_on_commit(
|
||||
self, db_session_expire_on_commit: AsyncSession
|
||||
):
|
||||
"""update()'s reload uses captured PK values, not an expired instance attribute.
|
||||
|
||||
Regression test for MissingGreenlet under expire_on_commit=True.
|
||||
"""
|
||||
UserWithDefaultLoad = CrudFactory(
|
||||
User, default_load_options=[selectinload(User.role)]
|
||||
)
|
||||
role = await RoleCrud.create(
|
||||
db_session_expire_on_commit, RoleCreate(name="admin")
|
||||
)
|
||||
user = await UserCrud.create(
|
||||
db_session_expire_on_commit,
|
||||
UserCreate(username="alice", email="alice@test.com"),
|
||||
)
|
||||
updated = await UserWithDefaultLoad.update(
|
||||
db_session_expire_on_commit,
|
||||
UserUpdate(role_id=role.id),
|
||||
filters=[User.id == user.id],
|
||||
)
|
||||
assert updated.role is not None
|
||||
assert updated.role.name == "admin"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_load_options_overrides_default_load_options(
|
||||
self, db_session: AsyncSession
|
||||
|
||||
+154
-10
@@ -372,6 +372,22 @@ class TestBuildSearchFilters:
|
||||
|
||||
assert len(joins) == 1
|
||||
|
||||
def test_skips_cast_on_string_column(self):
|
||||
"""String columns are filtered directly, without a CAST (keeps pg_trgm indexable)."""
|
||||
from fastapi_toolsets.crud.search import build_search_filters
|
||||
|
||||
filters, _ = build_search_filters(User, "john", search_fields=[User.username])
|
||||
|
||||
assert "CAST" not in str(filters[0])
|
||||
|
||||
def test_casts_non_string_column(self):
|
||||
"""Non-string columns (e.g. UUID) still get cast to String so ilike works."""
|
||||
from fastapi_toolsets.crud.search import build_search_filters
|
||||
|
||||
filters, _ = build_search_filters(User, "123", search_fields=[User.id])
|
||||
|
||||
assert "CAST" in str(filters[0])
|
||||
|
||||
|
||||
class TestSearchConfig:
|
||||
"""Tests for SearchConfig options."""
|
||||
@@ -531,6 +547,15 @@ class TestFacetsNotSet:
|
||||
|
||||
assert result.filter_attributes is None
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_build_facets_empty_field_list(self, db_session: AsyncSession):
|
||||
"""build_facets([]) is a no-op that returns {} without querying — the escape hatch."""
|
||||
from fastapi_toolsets.crud.search import build_facets
|
||||
|
||||
result = await build_facets(db_session, User, [])
|
||||
|
||||
assert result == {}
|
||||
|
||||
|
||||
class TestFacetsDirectColumn:
|
||||
"""Facets on direct model columns."""
|
||||
@@ -606,6 +631,91 @@ class TestFacetsDirectColumn:
|
||||
assert "username" not in result.filter_attributes
|
||||
|
||||
|
||||
class TestFacetsMixedTypes:
|
||||
"""Facet values keep their native Python type through the batched query."""
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_enum_and_integer_facets_preserve_types(
|
||||
self, db_session: AsyncSession
|
||||
):
|
||||
"""Enum facets return member names (not raw DB values); Integer facets return ints."""
|
||||
OrderMixedCrud = CrudFactory(
|
||||
Order, facet_fields=[Order.status, Order.priority, Order.color]
|
||||
)
|
||||
await OrderCrud.create(
|
||||
db_session,
|
||||
OrderCreate(
|
||||
name="order-1", status=OrderStatus.PENDING, priority=1, color=Color.RED
|
||||
),
|
||||
)
|
||||
await OrderCrud.create(
|
||||
db_session,
|
||||
OrderCreate(
|
||||
name="order-2", status=OrderStatus.SHIPPED, priority=3, color=Color.BLUE
|
||||
),
|
||||
)
|
||||
|
||||
result = await OrderMixedCrud.offset_paginate(db_session, schema=OrderRead)
|
||||
|
||||
assert result.filter_attributes is not None
|
||||
assert set(result.filter_attributes["status"]) == {"PENDING", "SHIPPED"}
|
||||
assert all(isinstance(v, str) for v in result.filter_attributes["status"])
|
||||
assert set(result.filter_attributes["priority"]) == {1, 3}
|
||||
assert all(isinstance(v, int) for v in result.filter_attributes["priority"])
|
||||
assert set(result.filter_attributes["color"]) == {"RED", "BLUE"}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bool_facet_keeps_python_bool(self, db_session: AsyncSession):
|
||||
"""A Boolean facet returns Python bool values, not stringified 'true'/'false'."""
|
||||
UserBoolFacetCrud = CrudFactory(User, facet_fields=[User.is_active])
|
||||
await UserCrud.create(
|
||||
db_session, UserCreate(username="alice", email="a@test.com", is_active=True)
|
||||
)
|
||||
await UserCrud.create(
|
||||
db_session, UserCreate(username="bob", email="b@test.com", is_active=False)
|
||||
)
|
||||
|
||||
result = await UserBoolFacetCrud.offset_paginate(db_session, schema=UserRead)
|
||||
|
||||
assert result.filter_attributes is not None
|
||||
assert set(result.filter_attributes["is_active"]) == {True, False}
|
||||
assert all(isinstance(v, bool) for v in result.filter_attributes["is_active"])
|
||||
|
||||
|
||||
class TestIncludeFacets:
|
||||
"""include_facets=False skips facet queries entirely."""
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_offset_paginate_include_facets_false(self, db_session: AsyncSession):
|
||||
"""filter_attributes is None when include_facets=False, even with facet_fields set."""
|
||||
UserFacetCrud = CrudFactory(User, facet_fields=[User.username])
|
||||
await UserCrud.create(
|
||||
db_session, UserCreate(username="alice", email="a@test.com")
|
||||
)
|
||||
|
||||
result = await UserFacetCrud.offset_paginate(
|
||||
db_session, include_facets=False, schema=UserRead
|
||||
)
|
||||
|
||||
assert result.filter_attributes is None
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_cursor_paginate_include_facets_false(self, db_session: AsyncSession):
|
||||
"""filter_attributes is None when include_facets=False for cursor_paginate."""
|
||||
UserFacetCursorCrud = CrudFactory(
|
||||
User, cursor_column=User.id, facet_fields=[User.username]
|
||||
)
|
||||
await UserCrud.create(
|
||||
db_session, UserCreate(username="alice", email="a@test.com")
|
||||
)
|
||||
|
||||
result = await UserFacetCursorCrud.cursor_paginate(
|
||||
db_session, include_facets=False, schema=UserRead
|
||||
)
|
||||
|
||||
assert result.filter_attributes is None
|
||||
|
||||
|
||||
class TestFacetsRespectFilters:
|
||||
"""Facets reflect the active filter conditions."""
|
||||
|
||||
@@ -630,6 +740,28 @@ class TestFacetsRespectFilters:
|
||||
assert result.filter_attributes is not None
|
||||
assert result.filter_attributes["username"] == ["alice"]
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_array_facet_respects_unrelated_filter(
|
||||
self, db_session: AsyncSession
|
||||
):
|
||||
"""An ARRAY facet is scoped by a filter on a different column (not self-collapse)."""
|
||||
ArticleFacetCrud = CrudFactory(Article, facet_fields=[Article.labels])
|
||||
await ArticleCrud.create(
|
||||
db_session, ArticleCreate(title="Post 1", labels=["python", "fastapi"])
|
||||
)
|
||||
await ArticleCrud.create(
|
||||
db_session, ArticleCreate(title="Post 2", labels=["rust", "axum"])
|
||||
)
|
||||
|
||||
result = await ArticleFacetCrud.offset_paginate(
|
||||
db_session,
|
||||
filters=[Article.title == "Post 1"],
|
||||
schema=ArticleRead,
|
||||
)
|
||||
|
||||
assert result.filter_attributes is not None
|
||||
assert result.filter_attributes["labels"] == ["fastapi", "python"]
|
||||
|
||||
|
||||
class TestFacetsRelationship:
|
||||
"""Facets on relationship columns via tuple syntax."""
|
||||
@@ -785,8 +917,8 @@ class TestFilterBy:
|
||||
|
||||
assert len(result.data) == 1
|
||||
assert result.data[0].username == "alice"
|
||||
# facet also scoped to the filter
|
||||
assert result.filter_attributes == {"username": ["alice"]}
|
||||
# facet excludes its own filter_by condition, so it isn't collapsed
|
||||
assert result.filter_attributes == {"username": ["alice", "bob"]}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_list_filter_produces_in_clause(self, db_session: AsyncSession):
|
||||
@@ -924,7 +1056,7 @@ class TestFilterBy:
|
||||
|
||||
assert len(result.data) == 1
|
||||
assert result.data[0].username == "alice"
|
||||
assert result.filter_attributes == {"username": ["alice"]}
|
||||
assert result.filter_attributes == {"username": ["alice", "bob"]}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_basemodel_filter_by_offset_paginate(self, db_session: AsyncSession):
|
||||
@@ -1085,8 +1217,10 @@ class TestFilterBy:
|
||||
assert result.pagination.total_count == 2
|
||||
titles = {a.title for a in result.data}
|
||||
assert titles == {"Post 1", "Post 3"}
|
||||
# facet returns individual unnested values, not whole arrays
|
||||
assert result.filter_attributes == {"labels": ["django", "fastapi", "python"]}
|
||||
# facet excludes its own filter_by condition (not collapsed to matching rows)
|
||||
assert result.filter_attributes == {
|
||||
"labels": ["axum", "django", "fastapi", "python", "rust"]
|
||||
}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_array_overlap_list_value(self, db_session: AsyncSession):
|
||||
@@ -2127,7 +2261,7 @@ class TestOffsetPaginateParamsSchema:
|
||||
def test_dependency_name_includes_model_name(self):
|
||||
"""Dependency function is named after the model."""
|
||||
dep = RoleCrud.offset_paginate_params(search=False, filter=False, order=False)
|
||||
assert getattr(dep, "__name__") == "RoleOffsetPaginateParams"
|
||||
assert dep.__name__ == "RoleOffsetPaginateParams" # type: ignore[union-attr] # ty:ignore[unresolved-attribute]
|
||||
|
||||
def test_default_page_size_reflected_in_items_per_page_default(self):
|
||||
"""default_page_size is used as the default for items_per_page."""
|
||||
@@ -2179,7 +2313,12 @@ class TestOffsetPaginateParamsSchema:
|
||||
include_total=False, search=False, filter=False, order=False
|
||||
)
|
||||
result = await dep(page=2, items_per_page=10)
|
||||
assert result == {"page": 2, "items_per_page": 10, "include_total": False}
|
||||
assert result == {
|
||||
"page": 2,
|
||||
"items_per_page": 10,
|
||||
"include_total": False,
|
||||
"include_facets": True,
|
||||
}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_integrates_with_offset_paginate(self, db_session: AsyncSession):
|
||||
@@ -2252,7 +2391,7 @@ class TestCursorPaginateParamsSchema:
|
||||
dep = RoleCursorCrud.cursor_paginate_params(
|
||||
search=False, filter=False, order=False
|
||||
)
|
||||
assert getattr(dep, "__name__") == "RoleCursorPaginateParams"
|
||||
assert dep.__name__ == "RoleCursorPaginateParams" # type: ignore[union-attr] # ty:ignore[unresolved-attribute]
|
||||
|
||||
def test_default_page_size_reflected_in_items_per_page_default(self):
|
||||
"""default_page_size is used as the default for items_per_page."""
|
||||
@@ -2290,7 +2429,11 @@ class TestCursorPaginateParamsSchema:
|
||||
search=False, filter=False, order=False
|
||||
)
|
||||
result = await dep(cursor=None, items_per_page=5)
|
||||
assert result == {"cursor": None, "items_per_page": 5}
|
||||
assert result == {
|
||||
"cursor": None,
|
||||
"items_per_page": 5,
|
||||
"include_facets": True,
|
||||
}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_integrates_with_cursor_paginate(self, db_session: AsyncSession):
|
||||
@@ -2318,7 +2461,7 @@ class TestPaginateParamsSchema:
|
||||
def test_dependency_name_includes_model_name(self):
|
||||
"""Dependency function is named after the model."""
|
||||
dep = RoleCursorCrud.paginate_params(search=False, filter=False, order=False)
|
||||
assert getattr(dep, "__name__") == "RolePaginateParams"
|
||||
assert dep.__name__ == "RolePaginateParams" # type: ignore[union-attr] # ty:ignore[unresolved-attribute]
|
||||
|
||||
def test_default_pagination_type(self):
|
||||
"""default_pagination_type is reflected in pagination_type default."""
|
||||
@@ -2399,6 +2542,7 @@ class TestPaginateParamsSchema:
|
||||
"cursor": None,
|
||||
"items_per_page": 10,
|
||||
"include_total": True,
|
||||
"include_facets": True,
|
||||
}
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -56,7 +56,7 @@ async def seed(session: AsyncSession):
|
||||
session.add_all([python, backend])
|
||||
await session.flush()
|
||||
|
||||
now = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
|
||||
now = datetime.datetime(2024, 1, 1, tzinfo=datetime.UTC)
|
||||
session.add_all(
|
||||
[
|
||||
Article(
|
||||
@@ -199,8 +199,9 @@ class TestOffsetPagination:
|
||||
resp = await client.get("/articles/offset?status=published")
|
||||
|
||||
body = resp.json()
|
||||
# draft is filtered out → should not appear in filter_attributes
|
||||
assert "draft" not in body["filter_attributes"]["status"]
|
||||
# a facet excludes its own filter_by condition, so filtering by
|
||||
# status=published still shows every status the facet offers
|
||||
assert "draft" in body["filter_attributes"]["status"]
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_search_and_filter_combined(self, client: AsyncClient, ex_db_session):
|
||||
|
||||
@@ -250,6 +250,7 @@ class TestDbExceptions:
|
||||
def test_pool_exhausted_handled_as_503(self):
|
||||
"""init_exceptions_handlers turns PoolExhaustedError into a 503 response."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||
|
||||
app = FastAPI()
|
||||
@@ -267,6 +268,7 @@ class TestDbExceptions:
|
||||
def test_lock_timeout_handled_as_503(self):
|
||||
"""init_exceptions_handlers turns LockTimeoutError into a 503 response."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from fastapi_toolsets.exceptions import init_exceptions_handlers
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -74,7 +74,7 @@ class TestCustomEnumContext:
|
||||
"""Python prohibits extending an Enum that already has members."""
|
||||
with pytest.raises(TypeError):
|
||||
|
||||
class MyContext(Context): # noqa: F841 # ty: ignore[subclass-of-final-class]
|
||||
class MyContext(Context): # ty: ignore[subclass-of-final-class]
|
||||
STAGING = "staging"
|
||||
|
||||
def test_custom_enum_values_interchangeable_with_context(self):
|
||||
|
||||
@@ -197,8 +197,8 @@ class TestCliImportGuard:
|
||||
importlib.import_module("fastapi_toolsets.cli.app")
|
||||
finally:
|
||||
for key in list(sys.modules):
|
||||
if key.startswith("fastapi_toolsets.cli.app") or key.startswith(
|
||||
"fastapi_toolsets.cli.config"
|
||||
if key.startswith(
|
||||
("fastapi_toolsets.cli.app", "fastapi_toolsets.cli.config")
|
||||
):
|
||||
sys.modules.pop(key, None)
|
||||
sys.modules.update(saved)
|
||||
|
||||
+39
-40
@@ -25,13 +25,11 @@ from fastapi_toolsets.models.watched import (
|
||||
_SESSION_CREATES,
|
||||
_SESSION_DELETES,
|
||||
_SESSION_UPDATES,
|
||||
_WATCHED_MODELS,
|
||||
EventSession,
|
||||
_after_flush,
|
||||
_after_rollback,
|
||||
_get_watched_fields,
|
||||
_invalidate_caches,
|
||||
_is_watched,
|
||||
_snapshot_column_attrs,
|
||||
_upsert_changes,
|
||||
)
|
||||
@@ -658,22 +656,6 @@ class TestWatchInheritance:
|
||||
assert "other" in _watch_inherit_events[0]["changes"]
|
||||
|
||||
|
||||
class TestIsWatched:
|
||||
def test_watched_model_is_watched(self):
|
||||
"""_is_watched returns True for models with registered handlers."""
|
||||
obj = WatchedModel(status="x", other="y")
|
||||
assert _is_watched(obj) is True
|
||||
|
||||
def test_non_watched_model_is_not_watched(self):
|
||||
"""_is_watched returns False for models without registered handlers."""
|
||||
assert _is_watched(object()) is False
|
||||
|
||||
def test_subclass_of_watched_model_is_watched(self):
|
||||
"""_is_watched returns True for subclasses of watched models (via MRO)."""
|
||||
dog = PolyDog(name="Rex")
|
||||
assert _is_watched(dog) is True
|
||||
|
||||
|
||||
class TestUpsertChanges:
|
||||
def test_inserts_new_entry(self):
|
||||
"""New key is inserted with the full changes dict."""
|
||||
@@ -715,7 +697,10 @@ class TestAfterFlush:
|
||||
"""New watched objects are added to _SESSION_CREATES."""
|
||||
obj = object()
|
||||
session = SimpleNamespace(new=[obj], deleted=[], dirty=[], info={})
|
||||
with patch("fastapi_toolsets.models.watched._is_watched", return_value=True):
|
||||
with patch(
|
||||
"fastapi_toolsets.models.watched._get_handlers",
|
||||
return_value=[lambda *a: None],
|
||||
):
|
||||
_after_flush(session, None)
|
||||
assert session.info[_SESSION_CREATES] == [obj]
|
||||
|
||||
@@ -731,7 +716,10 @@ class TestAfterFlush:
|
||||
obj = object()
|
||||
session = SimpleNamespace(new=[], deleted=[obj], dirty=[], info={})
|
||||
with (
|
||||
patch("fastapi_toolsets.models.watched._is_watched", return_value=True),
|
||||
patch(
|
||||
"fastapi_toolsets.models.watched._get_handlers",
|
||||
return_value=[lambda *a: None],
|
||||
),
|
||||
patch(
|
||||
"fastapi_toolsets.models.watched._snapshot_column_attrs",
|
||||
return_value={"id": 1},
|
||||
@@ -1023,28 +1011,19 @@ class TestEventCallbacks:
|
||||
await other.commit()
|
||||
await engine.dispose()
|
||||
|
||||
real_get = mixin_session.get
|
||||
real_refresh = mixin_session.refresh
|
||||
real_batch_reload = _watched_module._batch_reload
|
||||
|
||||
def _matches_doomed(pk):
|
||||
return pk == doomed_id or (isinstance(pk, tuple) and pk[0] == doomed_id)
|
||||
|
||||
async def racing_get(model, pk, *args, **kwargs):
|
||||
if _matches_doomed(pk):
|
||||
async def racing_batch_reload(session, model, pk_tuples):
|
||||
if any(pk[0] == doomed_id for pk in pk_tuples):
|
||||
await kill_doomed_row_once()
|
||||
return await real_get(model, pk, *args, **kwargs)
|
||||
return await real_batch_reload(session, model, pk_tuples)
|
||||
|
||||
async def racing_refresh(obj, *args, **kwargs):
|
||||
if getattr(obj, "id", None) == doomed_id:
|
||||
await kill_doomed_row_once()
|
||||
return await real_refresh(obj, *args, **kwargs)
|
||||
|
||||
# Patch both possible reload mechanisms (session.get / session.refresh)
|
||||
# so this test still exercises the race regardless of which one
|
||||
# EventSession.commit() uses internally to pick up server defaults.
|
||||
mixin_session.get = racing_get
|
||||
mixin_session.refresh = racing_refresh
|
||||
with patch.object(_watched_module._logger, "error") as mock_error:
|
||||
# Patch the batched reload EventSession.commit() uses to pick up
|
||||
# server defaults, so this test still exercises the race.
|
||||
with (
|
||||
patch.object(_watched_module, "_batch_reload", racing_batch_reload),
|
||||
patch.object(_watched_module._logger, "error") as mock_error,
|
||||
):
|
||||
await mixin_session.commit()
|
||||
mock_error.assert_not_called()
|
||||
|
||||
@@ -1052,6 +1031,27 @@ class TestEventCallbacks:
|
||||
created_ids = {e["obj_id"] for e in _test_events if e["event"] == "create"}
|
||||
assert created_ids == {keep.id, doomed_id}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_batch_reload_exception_is_logged_and_dispatch_continues(
|
||||
self, mixin_session
|
||||
):
|
||||
"""A batched-reload failure is logged; CREATE handlers still fire."""
|
||||
obj = WatchedModel(status="active", other="x")
|
||||
mixin_session.add(obj)
|
||||
|
||||
async def failing_batch_reload(session, model, pk_tuples):
|
||||
raise RuntimeError("reload failed")
|
||||
|
||||
with (
|
||||
patch.object(_watched_module, "_batch_reload", failing_batch_reload),
|
||||
patch.object(_watched_module._logger, "error") as mock_error,
|
||||
):
|
||||
await mixin_session.commit()
|
||||
|
||||
mock_error.assert_called_once()
|
||||
creates = [e for e in _test_events if e["event"] == "create"]
|
||||
assert len(creates) == 1
|
||||
|
||||
|
||||
class TestTransientObject:
|
||||
"""Create + delete within the same transaction should fire no events."""
|
||||
@@ -1421,7 +1421,6 @@ class TestListensFor:
|
||||
for key in list(_EVENT_HANDLERS):
|
||||
if key[0] is ListenerModel:
|
||||
del _EVENT_HANDLERS[key]
|
||||
_WATCHED_MODELS.discard(ListenerModel)
|
||||
_invalidate_caches()
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -13,6 +13,11 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
from fastapi_toolsets.db import transaction
|
||||
from fastapi_toolsets.fixtures import Context, FixtureRegistry, LoadStrategy
|
||||
from fastapi_toolsets.fixtures.utils import (
|
||||
_get_primary_key,
|
||||
_relationship_load_options,
|
||||
_reload_with_relationships,
|
||||
)
|
||||
from fastapi_toolsets.pytest import (
|
||||
create_async_client,
|
||||
create_db_session,
|
||||
@@ -20,11 +25,6 @@ from fastapi_toolsets.pytest import (
|
||||
register_fixtures,
|
||||
worker_database_url,
|
||||
)
|
||||
from fastapi_toolsets.fixtures.utils import (
|
||||
_get_primary_key,
|
||||
_relationship_load_options,
|
||||
_reload_with_relationships,
|
||||
)
|
||||
from fastapi_toolsets.pytest.utils import _get_xdist_worker
|
||||
|
||||
from .conftest import (
|
||||
|
||||
@@ -5,11 +5,11 @@ from pydantic import ValidationError
|
||||
|
||||
from fastapi_toolsets.schemas import (
|
||||
ApiError,
|
||||
CursorPagination,
|
||||
CursorPaginatedResponse,
|
||||
CursorPagination,
|
||||
ErrorResponse,
|
||||
OffsetPagination,
|
||||
OffsetPaginatedResponse,
|
||||
OffsetPagination,
|
||||
PaginatedResponse,
|
||||
PaginationType,
|
||||
Response,
|
||||
|
||||
@@ -192,101 +192,86 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.14.1"
|
||||
version = "7.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/3a/54536704f507d4573bf9161c4d0dd3dd59b6d85e48c664e901b6844d8e33/coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", size = 221414, upload-time = "2026-07-15T18:53:51.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d9/8ba925d29743e3577b21e4d8c11a702b76bc93c41e7fdfd1177af63d4b8d/coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", size = 221913, upload-time = "2026-07-15T18:53:53.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/54/a855f3aa0187f2b431ade4e4791b77b56282cfb5d201c83ec26a31b5b36a/coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", size = 252332, upload-time = "2026-07-15T18:53:55.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/d3/13ac97b4370640ba3452fc8559b06cc2f479ce3ba4a0b632a73e44c38a7d/coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", size = 254243, upload-time = "2026-07-15T18:53:57.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/83/5eca144942d8d0659d3f55176517f4a59cdc65eefd17146a0770935a3ebd/coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", size = 256352, upload-time = "2026-07-15T18:53:58.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/ba/d3db2e01a50fc88cdb4c0f19542bcf6f61489e34dc9aa3538413e2459a38/coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", size = 258313, upload-time = "2026-07-15T18:54:00.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b3/aba83416e9177df28e5186d856c19158c59fc0e7e814aaa61a4a2354ad1b/coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", size = 252449, upload-time = "2026-07-15T18:54:02.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/a5/4b00ecac0194431ab451b0f6710f8e2517d04cef60f821b14dec4637d575/coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", size = 254043, upload-time = "2026-07-15T18:54:04.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b6/cfa209b4313ee7f1b34da47efcd789ea51c024ad35af390e00f5a3c10a2e/coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", size = 252107, upload-time = "2026-07-15T18:54:06.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/67/e8cac5a6954038c98d7fe7eb9802afe7ab3ecb637bb7cc00e69b4148b56d/coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", size = 255873, upload-time = "2026-07-15T18:54:08.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/92/395cca9f330a86c3fe3471d73e2c102116c4c58fdc619dbbc125c6e93a54/coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", size = 251826, upload-time = "2026-07-15T18:54:10.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/60/3e91b20295439652424f426b7086ec5bf4fbe3f604c73eda22b986c4fd6b/coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", size = 252735, upload-time = "2026-07-15T18:54:11.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/eb/8c07839005e5e3c6b3877d3a6e2a80ce766589f31dd2b6882b78d59a7b8c/coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", size = 223500, upload-time = "2026-07-15T18:54:13.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/98/59d83c257cd59f0fbaf9d9ddb26b744a576760dfd1ae16e516408894a02b/coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", size = 223973, upload-time = "2026-07-15T18:54:15.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/09/2d285c8bef5c4f695d120c1c96dc11715638aa8e134069f210bb6a62a9fe/coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", size = 223519, upload-time = "2026-07-15T18:54:16.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -314,7 +299,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.136.3"
|
||||
version = "0.139.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -323,14 +308,14 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-toolsets"
|
||||
version = "5.0.0b2"
|
||||
version = "5.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "asyncpg" },
|
||||
@@ -829,26 +814,26 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "prek"
|
||||
version = "0.4.8"
|
||||
version = "0.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/46/e436a6eb9fdb4d3fd08d0ab7fdba19fe03a9e994ec810de57869b853bd8e/prek-0.4.8.tar.gz", hash = "sha256:d15d8bef72ab7b02c7dc01458ac9e05b3131534492b5ce9bb11c4f6f636fa868", size = 494570, upload-time = "2026-07-04T12:05:10.941Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/df/94ed29398576e03494c5aacda8bfed9536edf348bed29cd09f382f2b9b23/prek-0.4.9.tar.gz", hash = "sha256:f8b86441484a5756f3fdb6f3b201d3d448f8845902a84653d78cbf6f875c424f", size = 492711, upload-time = "2026-07-11T11:04:04.332Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/78/b4149c8913ced2e42debb49e261c4788a1ce431e84226921c2e1a7ea8545/prek-0.4.8-py3-none-linux_armv6l.whl", hash = "sha256:1f8f8cdc65836b571824c965daebb81b449f7e4a43894c58621f5708d5a185ed", size = 5668955, upload-time = "2026-07-04T12:04:41.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/5f/7f54a0087b6b2f1751aeb41266d9c15e66fd0055492814798ab818cd0414/prek-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bce1798e96d9e3a6e6abf435da7107e81452f69edb3ca7c6f90a457355ea46e2", size = 6030947, upload-time = "2026-07-04T12:04:43.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/d6/f2829fc3902920c36b764a386fa303e71a8219dac25cb3827c575e84199a/prek-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab3a52db17254d701c3cebb7eea58c8230aa7c1959aacfd5b5f25de18edb15d1", size = 5572593, upload-time = "2026-07-04T12:04:45.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/8c/c5589955bcd5e3e33b67d8bc3110818cecac82a38fd6bc8b5dfdc5de421c/prek-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b3fcfd620523bbc3f51a21d7cd63449f659b9e2cf3582de12dd5949e23227b8f", size = 5847150, upload-time = "2026-07-04T12:04:47.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/9d/1f2dc91bdb79d2c4714b27eac9477a51490fba5b4731330dbbebc76bd345/prek-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42e65bc8425e9d7f1691a13ca1da2e07807d1ba76c35740833354b945131689e", size = 5573738, upload-time = "2026-07-04T12:04:49.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/29/69a7b58e16ecbc5f3989bf4b028018d11a82dcdd320b93d6588d72f32aa7/prek-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f578492a8e0c9bc6b4bf6dfbba8716f647d4cd0769bf10ad6cf336e3096fd392", size = 5981054, upload-time = "2026-07-04T12:04:50.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/cc/9b9850a60c22ed18c7755ebd2d72c6eefb37fac58149d09f6adc4691c2cf/prek-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4335f9d5beb123a3884a7fe34f57c9f0828f4fbb7666beab4298833459b104f", size = 6751350, upload-time = "2026-07-04T12:04:52.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/e5/c425aa7272b430630119e6757def3a2007555ba8cbeb2630e0448e7a8b7f/prek-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a8747df9c602e052881d3efb14dd7f7d62a59bd7277ae5171c9e7661d59d84", size = 6243881, upload-time = "2026-07-04T12:04:54.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/da/accd3ad07fd2891d3c2777eb42435439fdf11982c51d60f087c0b6b6e102/prek-0.4.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4db639db481d5f854eff9b3d2108889e613b8c15868bcf6bdd777c7cee577436", size = 5848846, upload-time = "2026-07-04T12:04:56.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/00/3477704635249f21f5f98ce444cd7690c2aa9dc8d146a045db88ef2cd8c5/prek-0.4.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c3890a6f92316d2cf44eb50584e8d2b23a596dd70487022e61186a71a2ac0900", size = 5713942, upload-time = "2026-07-04T12:04:58.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e6/3ca4fabaebeadc976d9a92d1d9130674265355ea3b728418bad61583b097/prek-0.4.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fc7e15c24c591a37c6ffce5b25a021b16c299ac2649f183d812b67d665cd6551", size = 5554725, upload-time = "2026-07-04T12:04:59.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/46/2ab6aaaeff0cedb8955b2e4032071c8712382bdd423bb849718c3720180d/prek-0.4.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:36fe721704ff0c7624c1167639e23a5fe658bfd38c314f487219c9afd1eeb733", size = 5838595, upload-time = "2026-07-04T12:05:01.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8b/91398f2b6cd1629d5d8ca8c85b08eca500814a374313b0193f4aaf6ab6c4/prek-0.4.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:162e544abc394a8124f3a4ad68efee116bad09440e679dbd1675177335c2a432", size = 6357222, upload-time = "2026-07-04T12:05:03.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/2a/ce5cbfaad36866134a21754640a05ecdba641fcd7ad15aa74cf3443f34f6/prek-0.4.8-py3-none-win32.whl", hash = "sha256:2602e46c8c5da7dfa69f60fcf88c2b57132ac623f49fb08bfb3094298c5f07e3", size = 5354388, upload-time = "2026-07-04T12:05:05.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/03/3bc908bc5f7e430315553e47dfa055f19923a3888f9afe4da19f244b5cbf/prek-0.4.8-py3-none-win_amd64.whl", hash = "sha256:7cb22da60bee41b89c4978c0bea7126a3c0ccc003dae6748cf29b53947815edc", size = 5748221, upload-time = "2026-07-04T12:05:07.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/a7/4295e6d5f5028171dfeb115ad38ab76bf3fe0c8df91b70d73c79aa760a94/prek-0.4.8-py3-none-win_arm64.whl", hash = "sha256:da70057f577b15d4bd121bf9dd29ee205fd4b4d75a0cafba062e84d7e8b4378b", size = 5574425, upload-time = "2026-07-04T12:05:09.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/02/632446b72103daf92526203bf83cb76043ebce70588632aa2aea3741da07/prek-0.4.9-py3-none-linux_armv6l.whl", hash = "sha256:7b240ad6f679104309a944c4dd427ccc46d9aaf4f53ee07379c02bf7578c2750", size = 5637604, upload-time = "2026-07-11T11:03:38.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/bf/89daefc85a1db9b8c525731c77121de0a8f57731e81c99d823e919e1f6a5/prek-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5cbb220d6d77cb047747dcabf421375fdfdd958e01a998a854758698d38fb239", size = 5960396, upload-time = "2026-07-11T11:03:40.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/39/0e448da00671e77740be32cca96530ef64bcdbed178acce7f155b87f6057/prek-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd85df4c186becdc47b2e6155de8cace99133c7517387e30f08ca15b93ead11f", size = 5524982, upload-time = "2026-07-11T11:03:42.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e5/1beceff9cfcf02817c06f38e726c9875cd003f62cbfa516f282fb3c3109b/prek-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ba40511145e948d461d6641b556b6ff671b186b0c90743600b9dcb788dd5eb5c", size = 5793691, upload-time = "2026-07-11T11:03:44.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/17/99e4884c45c46be90e81e220d2bdd5020716963707ef6702361cc398dd91/prek-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0cc5c06ae448568d076bb5e7ce4d630879d6467b2a89fd79061c349a47826efa", size = 5544549, upload-time = "2026-07-11T11:03:45.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/4b/63f5fe22d867a6e07b12e8a7887a0f3b193c2ef0fa9ed80649f2d257f4ac/prek-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31af616c216ec7e47913802b082ca816d707d24738fa58eae0463ae296cbe71e", size = 5948798, upload-time = "2026-07-11T11:03:47.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/89/90d5005436afb6ab99d3ba6599820fe0f0fcb776f5e9e362b5a19e10cbea/prek-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1ef842d7f19879fac2135c42ba15508f2677346ba800bc8bb82e5f43fe6a6ec", size = 6696938, upload-time = "2026-07-11T11:03:49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/62/068dd25e1106e262b0ce69ffcdc594cf52d85aa13f64628f851e458980d4/prek-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442ecf6a454c692bc8a2d5a16936a0fe8e3419727ff208e15818932acca9923a", size = 6170870, upload-time = "2026-07-11T11:03:50.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/42/bb1f3f3d84af4d12bb675ff071305fa776d3525c10626465d9960ad93c21/prek-0.4.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5b3c590252e3d5724ebed3774695712ff7cb554743b25184808aa5f42b06bd4c", size = 5800355, upload-time = "2026-07-11T11:03:51.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ca/dfc8312b4a7ce8fa100ce37a843279d108eec7e7fe2ddbe069448acd1642/prek-0.4.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dd283b15dec4da29caa3910bd72c8c9d7df93209770503465ad109d6370cb8e", size = 5655126, upload-time = "2026-07-11T11:03:53.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8a/b19413cb64b81c18502ea7bbef32897f9126b8e53b58cd656996573dc53c/prek-0.4.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:cc25e30e1700a5c7dd9bad665c321e5589b51502bb1bbc6ada45e326d08b428b", size = 5517839, upload-time = "2026-07-11T11:03:54.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/af/900df3f7535e87045df331646c6a01bef6e77dba7f2bdb53483f30cbb988/prek-0.4.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4ff5947deeb9a92e6508dfba8b27962dfb927bf60fd36472c9ae862df96fb38c", size = 5802556, upload-time = "2026-07-11T11:03:56.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/7b/80e560cbe396d0f8687012769cb2d7d7f3428dd019549225ebf205dea806/prek-0.4.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8320ca167d41855d9c4fed66df599f31f96307cbb0da1311a9fe465152e20bd5", size = 6285747, upload-time = "2026-07-11T11:03:58.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/d4/01ae3b99d09559a69befd128859d0036c604608dd9c6c99986592dde3c21/prek-0.4.9-py3-none-win32.whl", hash = "sha256:b1e8d3bc88ddce6414853468ed8126f45d4ae20f2f4677801ade20ad67a826fa", size = 5320862, upload-time = "2026-07-11T11:03:59.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/68/d038b14f0220fed197be8bc93229e6ea7ad460803ff8a26e4b14a8c81f66/prek-0.4.9-py3-none-win_amd64.whl", hash = "sha256:ed1b4f87a13d1565e8731c60db7fa058966049cbb4d8872d160add510a286558", size = 5706850, upload-time = "2026-07-11T11:04:01.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/4a/57d04de49f591088901794cc22f36563a102681e3512ae17ee6085cd2f30/prek-0.4.9-py3-none-win_arm64.whl", hash = "sha256:7eab3900d9ea614c8ea0d0d55a8b708f0c88e43c966dc8b13a4e36c1e398dd16", size = 5540477, upload-time = "2026-07-11T11:04:02.815Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -988,15 +973,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "10.21.3"
|
||||
version = "11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/67/f1e79672a5f91985577c7984c9709ca110e4fd37fe7fd167b60422e6ccc2/pymdown_extensions-11.0.tar.gz", hash = "sha256:8269cef0247f9e2d0a62fcea10860aba05c1cbab5470fd4b63230b96434dc589", size = 857049, upload-time = "2026-06-23T02:27:45.146Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl", hash = "sha256:fbc4acb641814fa9d17521bbd21a5240ef739a662f11c06330c4b78c93e954d6", size = 269415, upload-time = "2026-06-23T02:27:43.826Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1010,7 +995,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -1019,9 +1004,9 @@ dependencies = [
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1158,27 +1143,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.21"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1322,32 +1307,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.58"
|
||||
version = "0.0.64"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/4c/26c90732658903aeb1d289208f7b7b492fa21029e0c4d6c51bdd6f8f5e51/ty-0.0.58.tar.gz", hash = "sha256:8f22484174e65c630660a454bf81b80cae7a3a7e70479f19c170d6cd87949258", size = 6133665, upload-time = "2026-07-10T03:09:30.542Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/e1/5d1aa2a75829459834689f080e4be7a9d8828ce14b939ebed69161a35811/ty-0.0.58-py3-none-linux_armv6l.whl", hash = "sha256:47412850b6fbef61c42f244f6a51aa2f2c9e91f08cfbafd2d1e3730d2419d317", size = 11706915, upload-time = "2026-07-10T03:08:51.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e1/929eda9cc72a9afe39a03c76f946a503508d37343cc8ff2e64226afda105/ty-0.0.58-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79deb7bb4e5b3a1eee6ab9abc724d6ce3559d4977982707f310a139ee11fc703", size = 11532079, upload-time = "2026-07-10T03:08:53.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/43/ebc58b3fc7d86a7abba2829f1674f7d4ae3a08f9794c1f31b707950c871f/ty-0.0.58-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a28af3187e661708a386d44a4fc32896a5f589fb07b734a11ab2f516e7572b7", size = 11092983, upload-time = "2026-07-10T03:08:56.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/6e/9547dbb8e51e47749cfb721a02b4fc862f9a932fa0f66a34a4d6dc429bb1/ty-0.0.58-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21a6977e34bc362fb378add46e59d5d56331c1c36727e6904767217ca8479718", size = 11490492, upload-time = "2026-07-10T03:08:58.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/76/9f83b51b5e7795d6c8d76b4bb1bb7cebf4c10d609e846c02ab138e404556/ty-0.0.58-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ac23e6bf6105ceca46632debf1b10b98125aaf60202aeae02f6abfeea242b3d", size = 11503696, upload-time = "2026-07-10T03:09:00.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/9d/9fd48a0696c680f74e50f31cd54e524eeb58c97ea9bb1c3b8e04230ba215/ty-0.0.58-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb6df6a8c6a21894807a49851370ec7fc64aa910296c78ada31db0ef19359112", size = 12158653, upload-time = "2026-07-10T03:09:02.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/ea/b5de845d2d8edae04d901c7585af7f04a057e18b26311f7fa4ca62b2da30/ty-0.0.58-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9df5847eebc026cde088b44420a03f7c6c169a7db747176bdf0a656eb1144713", size = 12723019, upload-time = "2026-07-10T03:09:05.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/1c/54083b23eeff1e101f50b6df6a2c7f1e14b31abe0577c91bcae9e2c9395d/ty-0.0.58-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53a331a7f1f85872c810676a0f16096ef98c1b95c8c9a573fe7fde64d0a93e7c", size = 12275715, upload-time = "2026-07-10T03:09:07.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/88/16925434b06faa49d36aa7e7508a6821ec6feacb429ca2fd80a3d52716b4/ty-0.0.58-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0b05cd479fdcedc2e6781d376ee1a33569f37ae7f58357004635f615c4374c", size = 12033075, upload-time = "2026-07-10T03:09:10.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/2b/b55708dd483982ae03d14da3667e3f0346cd384e11cca3dd3674a8b598c1/ty-0.0.58-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a0012786077e5becbb6add9fe51eda1d4d36d249afd3ae6cd141d554af15ae3", size = 12367729, upload-time = "2026-07-10T03:09:12.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/a3/99ad66652956408f7e9ac3db6b4a416199d773b6c073cf95b0eea126d340/ty-0.0.58-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4ede96d7f6d149156da254e784b062b817736b68d4c6d660555a3d02d96966fb", size = 11439798, upload-time = "2026-07-10T03:09:14.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/23/344ceed4fe02ed498711e1f4a47b6e311fb1e9c4fecc19d31894be7a3472/ty-0.0.58-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:47608e58f73901b989402e6f283249cda4c2314282ec368aa74ab61761c62bd5", size = 11512695, upload-time = "2026-07-10T03:09:16.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/cf/3801831812c468f3fd0b3043a80f557a9aa90e6c27375763d7c3121e03f2/ty-0.0.58-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9757de17cc17e4c6bc26e18d4e26dea52ffee53d41a10d9087c6465e6ab12e2b", size = 11812253, upload-time = "2026-07-10T03:09:19.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/80/bce4f245787b77d1ec9feec7d9161eade5e01a77dbc132e016b24df83b0d/ty-0.0.58-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f3776d54c1d935fcb8f814bd58efba402c86c555f93e1144c59d087e7aa8b906", size = 12123918, upload-time = "2026-07-10T03:09:21.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/00/bab3d6268e7e88c792bf7cdde81bd11b31aa587eaba75196502b729747c8/ty-0.0.58-py3-none-win32.whl", hash = "sha256:8f50ec0ac3b42baa4c75895dc367071dc86b00ab440d29fdc72c633286a94815", size = 11230897, upload-time = "2026-07-10T03:09:23.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/68/d9504c895864aaa84a840dce6ac7f8e681f6938be1882c8d4f60832dbe57/ty-0.0.58-py3-none-win_amd64.whl", hash = "sha256:de8847b3a65475ae4773bddd3126bfcf29f017e88967c4dcc9c75d743a4d3e5c", size = 12299376, upload-time = "2026-07-10T03:09:26.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/04/c9847cb680b5fe8e1f7d7b483edd5cedcc29394496e7b8ed40d96be796ba/ty-0.0.58-py3-none-win_arm64.whl", hash = "sha256:7334bb38789878f60677f2eb9c1de4bfdf4583e2443790989c77da9b10fe0989", size = 11710495, upload-time = "2026-07-10T03:09:28.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.26.7"
|
||||
version = "0.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -1355,9 +1340,9 @@ dependencies = [
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/ed/ef06584ccdd5c410df0837951ecd7e15d9a6144ea1bd4c73cecab1a89891/typer-0.26.7.tar.gz", hash = "sha256:e314a34c617e419c091b2830dda3ea1f257134ff593061a8f5b9717ab8dddb3a", size = 201709, upload-time = "2026-06-03T07:18:06.843Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/2201973529af2c954de0bb725323c3aaed6d7f0ceee8f550dec9185df013/typer-0.26.7-py3-none-any.whl", hash = "sha256:5c87cfbc5d34491c5346ebf49c23e18d56ccb863268d3a8d592b26087c2f5e58", size = 122456, upload-time = "2026-06-03T07:18:05.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1419,7 +1404,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "zensical"
|
||||
version = "0.0.50"
|
||||
version = "0.0.51"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
@@ -1431,18 +1416,18 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tomli" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/79/f7959b13c766a831f1248779bb718555f70f40c5b8e68db7de1de9936662/zensical-0.0.50.tar.gz", hash = "sha256:7040e52ebe5e6a275e4edeb351bf2bc314d007f3fb5750f178a38d840723e69c", size = 3979246, upload-time = "2026-07-09T13:52:11.908Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b8/f7/d07ffb268ca86afb26b7f32dbabe25dec03d3aa63ba4d876720c84681d33/zensical-0.0.51.tar.gz", hash = "sha256:de25de067bedfa18f916d7f366fd64a7fbf09bfcc615b44d1ddbe3b5fe02ab49", size = 3979640, upload-time = "2026-07-17T18:08:03.445Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/d2/e126d56642a5fd437eeb5a067b7c0d1f766c08e22d6a708bb923986f1d44/zensical-0.0.50-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:26dda9dbacab84db2743726f83202798e1893fbfddecb6a845812d7a331043ad", size = 12817706, upload-time = "2026-07-09T13:51:29.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9b/e1a22fb4ea8fc3e8377d7b394ee06cf0349e7f9a64ecb4c9873dee79b102/zensical-0.0.50-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:376887def6470c57509b48519870f74e2b987e30509cf5c819fcf372771f9403", size = 12691061, upload-time = "2026-07-09T13:51:33.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/f5/16c087635680efb39e1d7b7aa3c2cca548305aa29ad51dee04f37b32b65f/zensical-0.0.50-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbf3034a2cb1a9d2a72a5ee7047688717c93da0d8a90d25dd871db8d5f33a6d", size = 13129505, upload-time = "2026-07-09T13:51:36.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/50d02402e53aa01a5c5c116a3522c7bb18d6ae4da964d8b236173b6fa086/zensical-0.0.50-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26fe4167663d544ca385ef420a49d3b738c7606a79d7a62a6bedcbf74cd383e7", size = 13055300, upload-time = "2026-07-09T13:51:39.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/0f/45f4f3e56a6483ea9f88478b20d6b434718e7c46579239589202e5135ec1/zensical-0.0.50-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8813d1f8895b19520d4a982d6bfcfd06372d73d96e6a63f0d2cf8f3b6036d5a1", size = 13445447, upload-time = "2026-07-09T13:51:43.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9b/1f5065c664afde6a63e0ada8c14eae75f9e6960df597a5ca28c07340f938/zensical-0.0.50-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a64f957f94f7d8e8847a7f1e8205509ba4b188c93c157c5e7be49bcb8556a127", size = 13098775, upload-time = "2026-07-09T13:51:46.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/65/1e42e18d8f9a0cb9fcc8b871852810c7fe196237b79aaa831e863325091e/zensical-0.0.50-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:781d333bcb42c6a6b92360e05eadc944e2674794e1a95537971de1bf64cb9f89", size = 13305922, upload-time = "2026-07-09T13:51:51.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/36/9cace194ba13aef3bdde8ca2d327f3a05cdf5c456e94f6d896852e2cbd38/zensical-0.0.50-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5d0f86eec76c23efb04566cd444bf025845ac8a02e75c8a6b4af13a39a2a9955", size = 13325488, upload-time = "2026-07-09T13:51:54.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/80/2891a9316e486794dae43151561b49f3313499cfb2c95c7886abad7bcf2d/zensical-0.0.50-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:392a7299a3c4d1ac7ff288bbed181d77a9fd23e6614180476e01630aaee0d67e", size = 13496866, upload-time = "2026-07-09T13:51:58.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/09/fbd3af58081a0d399fa913ef1f2fd6a007f2f06f7026c8d6a6096e34b996/zensical-0.0.50-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cac3448d8c8a76dfb43e55908b4ca9ad17596aee89a9287a184c1a0ba7dce2a2", size = 13437565, upload-time = "2026-07-09T13:52:01.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/79/709311ff202326260ff223fb6e9ddefa3100474c428e3c70a04036cefe0e/zensical-0.0.50-cp310-abi3-win32.whl", hash = "sha256:32b244f96a930f68e049b2e03d50790c120ef701b6277ddd854905a33040c75f", size = 12371966, upload-time = "2026-07-09T13:52:05.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/e1/4babb30544d7465c95a6a93abc0680b1c51752411a225c2b7f2cb44e80c7/zensical-0.0.50-cp310-abi3-win_amd64.whl", hash = "sha256:2f054769bf96ae46353de3eca2463ad4db6df1da9fde515c6d7fc54c3608e615", size = 12604832, upload-time = "2026-07-09T13:52:09.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/21/02db3e1fb3904016bfac310037c95b9f1eaaf0ffe7b4a84f14263a7d95df/zensical-0.0.51-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:134d776afa526098e05e34713e2f577c075e57a232e01b97842bb0206716afce", size = 12791154, upload-time = "2026-07-17T18:07:20.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/35/b0d96f58253514cb3d08f5779020ab01ee5472334fb984b92e3fc9e9c9ac/zensical-0.0.51-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e97ab39668ae3b452c550634e921a0336443743aae5e1fe031c7bb57d049e535", size = 12692190, upload-time = "2026-07-17T18:07:24.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/90/7a60e126a10c37c6b789938ff17e73fe76bba707fa029cb40ac659aeaa82/zensical-0.0.51-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c9579809f88608e7aa2cff516fff9d267d74a843cf6088a5f4227de2f092bb5", size = 13139337, upload-time = "2026-07-17T18:07:27.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/c3/9101c97b90d4713ef2816db03366a45ae4762efebffd296737a2dd2df325/zensical-0.0.51-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296dc7a14aa28b81a58eb57df2d5c9c9a4b0de7e90c11d99c943354287952925", size = 13069851, upload-time = "2026-07-17T18:07:31.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/79/0474df9e15a2c18f6281a786e10177c1b6e16feac1c568e7f36ad39b339c/zensical-0.0.51-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f779d2d87b4bf228cf2e279bc0ae6bcf3b36a9335ff283a317d01f7c15ae46b2", size = 13451083, upload-time = "2026-07-17T18:07:35.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/6f/91bbf78f704d5fd4c0c9be27d6bce3b6e4c2c339e4dcd6e7cf19ecda643c/zensical-0.0.51-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f813a1514a90890ca86248a8d54b81b2164bcbff11a6bcf11b01e1c01a1454", size = 13110446, upload-time = "2026-07-17T18:07:38.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/89/aa9a95f81771614c37bdc52b8ab21fcdef4c8de7c9cedf34e9bf62674281/zensical-0.0.51-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:186ef37e0eee0e969e2cfae47b1b97775e3164e2cba95c71faa4dd6ef47ed009", size = 13315871, upload-time = "2026-07-17T18:07:42.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/11/1bf6e9ded29d376f8c12644cc4de04676b010fee8caa17f682606b1f16d5/zensical-0.0.51-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5d91ce246ed930224603083cef02ae8947132fc7c52901d72015ea03526fa58", size = 13344382, upload-time = "2026-07-17T18:07:46.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/ec/663f16ff82d08b212e7c3236a88bd332f73331f94ddac1c91aaf882bbd1e/zensical-0.0.51-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1108eae82c6e8ffc33026f60b485c1512647a5333be4f547166b7c8877b98af", size = 13499628, upload-time = "2026-07-17T18:07:49.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/b4/7f1b6c3cf06d9f6ff5216523168a5d6ccc693444d5ceeb911eca97b30d98/zensical-0.0.51-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6fa0ecaf14f56841bfc595fa141396350c72aafbec73a016ebe3c824ed21ac72", size = 13451420, upload-time = "2026-07-17T18:07:52.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/44/be4bc09ec8f69e7be1b07b875887961c4e0e478b03a10d2cc624ef28fbe6/zensical-0.0.51-cp310-abi3-win32.whl", hash = "sha256:fb7ff4946b72168759c6af0a29cf5de4c38aebe633a83292e8cd4145b5213cc2", size = 12375639, upload-time = "2026-07-17T18:07:56.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/85/aa827c244ed4f404e99a91c3ecf5e5adb62eca806a9e9c8e3333bbad8660/zensical-0.0.51-cp310-abi3-win_amd64.whl", hash = "sha256:12529d3d3991b63820952111dc1d1edc29b2c9b3a3abb16c243bcb649631ebf2", size = 12628965, upload-time = "2026-07-17T18:07:59.741Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user