mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-09-19 11:19:56 +00:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca668f040b | ||
|
|
e09b911277 | ||
|
|
f7ecb76e8d
|
||
|
|
ef269833b9
|
||
|
|
610b3e1ab4
|
||
|
|
312723d66b
|
||
|
|
3d426ac567 | ||
|
|
0cc189117d
|
||
|
|
a7b78832fd | ||
|
|
56c19971e2 | ||
|
|
7cd0ca2936 | ||
|
|
713e9a40c8 | ||
|
|
1432a2cc3a | ||
|
|
375d349fc4 | ||
|
|
7506fa3093 | ||
|
|
943115562b
|
||
|
|
716e4f7db7 | ||
|
|
50529b1081
|
||
|
|
8aea7247c9 | ||
|
|
654347126d
|
||
|
|
f1e50a947a | ||
|
|
6dafd40277 | ||
|
|
1c806cccd9
|
||
|
|
1354f59bb4 | ||
|
|
23dc5c86b2 | ||
|
|
7c73488f7d | ||
|
|
518e22e921 |
@@ -373,6 +373,16 @@ Or via the dependency to narrow which fields are exposed as query parameters:
|
|||||||
params = UserCrud.offset_paginate_params(search_fields=[Post.title])
|
params = UserCrud.offset_paginate_params(search_fields=[Post.title])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`search_fields`, `facet_fields` and `order_fields` follow the same override rule
|
||||||
|
everywhere they are accepted — `offset_paginate`, `cursor_paginate`,
|
||||||
|
`paginate` and the matching `*_paginate_params` dependencies:
|
||||||
|
|
||||||
|
| Passed | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| omitted or `None` | Use the class-level declaration |
|
||||||
|
| `[]` | Disable this feature for this call |
|
||||||
|
| `[...]` | Use exactly these fields (the primary key is **not** prepended — that only happens for the class-level `searchable_fields`) |
|
||||||
|
|
||||||
This allows searching with both [`offset_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.offset_paginate) and [`cursor_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.cursor_paginate):
|
This allows searching with both [`offset_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.offset_paginate) and [`cursor_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.cursor_paginate):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ db = Database(
|
|||||||
|
|
||||||
## Committing before the response
|
## Committing before the response
|
||||||
|
|
||||||
[`db.install(app)`](../reference/db.md#fastapi_toolsets.db.Database) adds a middleware that commits the request's session when the response starts, after the endpoint returns and before the body is sent. With the middleware installed, the dependency does not commit again.
|
[`db.install(app)`](../reference/db.md#fastapi_toolsets.db.Database) adds a middleware that commits the request's session when the response starts, after the endpoint returns and before the body is sent. The dependency commits only if the middleware did not: when a function-scoped dependency unwinds before the response, or when the response never passes through the middleware. Either way the request is committed exactly once.
|
||||||
|
|
||||||
The request is committed as a single transaction:
|
The request is committed as a single transaction:
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,36 @@ async def create_user(body: UserCreateSchema, role: Role = RoleDep):
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Eager loading
|
||||||
|
|
||||||
|
By default both factories fetch through a bare `CrudFactory(model)`, so relationships are not loaded. Pass `load_options` for a one-off, or `crud` to reuse a CRUD class you already configured:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from fastapi_toolsets.crud import CrudFactory
|
||||||
|
from fastapi_toolsets.dependencies import PathDependency
|
||||||
|
|
||||||
|
UserDep = PathDependency(
|
||||||
|
model=User,
|
||||||
|
field=User.id,
|
||||||
|
session_dep=get_db,
|
||||||
|
load_options=[selectinload(User.role)],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Or reuse the app's configured CRUD and its default_load_options
|
||||||
|
UserCrud = CrudFactory(User, default_load_options=[selectinload(User.role)])
|
||||||
|
UserDep = PathDependency(model=User, field=User.id, session_dep=get_db, crud=UserCrud)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}")
|
||||||
|
async def get_user(user: User = UserDep):
|
||||||
|
return user.role.name # already loaded, no extra query
|
||||||
|
```
|
||||||
|
|
||||||
|
Both parameters work the same way on `BodyDependency`. When given together, the
|
||||||
|
usual [relationship loading](crud.md#relationship-loading) precedence applies.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[:material-api: API Reference](../reference/dependencies.md)
|
[:material-api: API Reference](../reference/dependencies.md)
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "fastapi-toolsets"
|
name = "fastapi-toolsets"
|
||||||
version = "5.1.0"
|
version = "5.1.2"
|
||||||
description = "Production-ready utilities for FastAPI applications"
|
description = "Production-ready utilities for FastAPI applications"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -91,7 +91,7 @@ docs-src = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.10,<0.12.0"]
|
requires = ["uv_build>=0.10,<0.13.0"]
|
||||||
build-backend = "uv_build"
|
build-backend = "uv_build"
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
@@ -101,7 +101,7 @@ exclude = ["*.md"]
|
|||||||
extend-select = ["E712"]
|
extend-select = ["E712"]
|
||||||
|
|
||||||
[tool.ruff.lint.flake8-bugbear]
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
extend-immutable-calls = ["fastapi.Depends"]
|
extend-immutable-calls = ["fastapi.Depends", "fastapi.Security"]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"tests/**" = ["RUF012", "RUF059", "SIM117", "DTZ001", "S110", "BLE001"]
|
"tests/**" = ["RUF012", "RUF059", "SIM117", "DTZ001", "S110", "BLE001"]
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ Example usage:
|
|||||||
return Response(data={"user": user.username}, message="Success")
|
return Response(data={"user": user.username}, message="Success")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "5.1.0"
|
__version__ = "5.1.2"
|
||||||
|
|||||||
@@ -14,12 +14,25 @@ from typing import Any, ClassVar, Generic, Literal, Self, TypeAlias, cast, overl
|
|||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import Date, DateTime, Float, Integer, Numeric, Uuid, and_, func, select
|
from sqlalchemy import (
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Float,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
Uuid,
|
||||||
|
and_,
|
||||||
|
func,
|
||||||
|
select,
|
||||||
|
tuple_,
|
||||||
|
)
|
||||||
from sqlalchemy.dialects.postgresql import insert
|
from sqlalchemy.dialects.postgresql import insert
|
||||||
from sqlalchemy.exc import NoResultFound
|
from sqlalchemy.exc import NoResultFound
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import DeclarativeBase, QueryableAttribute, selectinload
|
from sqlalchemy.orm import DeclarativeBase, QueryableAttribute, selectinload
|
||||||
|
from sqlalchemy.sql import operators
|
||||||
from sqlalchemy.sql.base import ExecutableOption
|
from sqlalchemy.sql.base import ExecutableOption
|
||||||
|
from sqlalchemy.sql.elements import UnaryExpression
|
||||||
from sqlalchemy.sql.roles import WhereHavingRole
|
from sqlalchemy.sql.roles import WhereHavingRole
|
||||||
|
|
||||||
from ..db import transaction
|
from ..db import transaction
|
||||||
@@ -128,6 +141,32 @@ def _apply_joins(q: Any, joins: JoinType | None, outer_join: bool) -> Any:
|
|||||||
return q
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
def _fans_out(
|
||||||
|
search_joins: Sequence[Any] | None, order_joins: Sequence[Any] | None
|
||||||
|
) -> bool:
|
||||||
|
"""True if any relationship join yields a collection."""
|
||||||
|
return any(
|
||||||
|
rel.property.uselist for rel in (*(search_joins or ()), *(order_joins or ()))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _grouped_order(clause: Any, table: Any) -> Any:
|
||||||
|
"""Recast an order clause for a query grouped by the entity's key."""
|
||||||
|
inner = clause.element if isinstance(clause, UnaryExpression) else clause
|
||||||
|
expr = inner.__clause_element__() if hasattr(inner, "__clause_element__") else inner
|
||||||
|
tables = {
|
||||||
|
t
|
||||||
|
for c in getattr(expr, "base_columns", ())
|
||||||
|
if (t := getattr(c, "table", None)) is not None
|
||||||
|
}
|
||||||
|
if tables and tables <= {table}:
|
||||||
|
return clause
|
||||||
|
agg = func.min(inner)
|
||||||
|
if isinstance(clause, UnaryExpression) and clause.modifier is operators.desc_op:
|
||||||
|
return agg.desc()
|
||||||
|
return agg.asc()
|
||||||
|
|
||||||
|
|
||||||
class AsyncCrud(Generic[ModelType]):
|
class AsyncCrud(Generic[ModelType]):
|
||||||
"""Generic async CRUD operations for SQLAlchemy models.
|
"""Generic async CRUD operations for SQLAlchemy models.
|
||||||
|
|
||||||
@@ -163,6 +202,64 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
):
|
):
|
||||||
cls.searchable_fields = [pk_col, *raw_fields]
|
cls.searchable_fields = [pk_col, *raw_fields]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pk_attrs(cls: type[Self]) -> list[QueryableAttribute[Any]]:
|
||||||
|
"""The model's primary key columns as instrumented attributes."""
|
||||||
|
return [
|
||||||
|
getattr(cls.model, cast(str, col.key))
|
||||||
|
for col in cls.model.__mapper__.primary_key
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _page_entities(
|
||||||
|
cls: type[Self],
|
||||||
|
session: AsyncSession,
|
||||||
|
q: Any,
|
||||||
|
*,
|
||||||
|
order_clauses: Sequence[Any],
|
||||||
|
limit: int,
|
||||||
|
offset: int | None = None,
|
||||||
|
load_options: Sequence[ExecutableOption] | None = None,
|
||||||
|
with_for_update: _ForUpdateMode = False,
|
||||||
|
) -> list[ModelType]:
|
||||||
|
"""Return up to *limit* entities, paging over distinct primary keys."""
|
||||||
|
pk_attrs = cls._pk_attrs()
|
||||||
|
table = cls.model.__table__
|
||||||
|
# Fall back to the primary key so the page boundary is deterministic.
|
||||||
|
grouped = [_grouped_order(c, table) for c in order_clauses] or [pk_attrs[0]]
|
||||||
|
id_q = (
|
||||||
|
q.order_by(None)
|
||||||
|
.with_only_columns(*pk_attrs)
|
||||||
|
.group_by(*pk_attrs)
|
||||||
|
.order_by(*grouped)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
if offset:
|
||||||
|
id_q = id_q.offset(offset)
|
||||||
|
rows = (await session.execute(id_q)).all()
|
||||||
|
ids = [row[0] if len(pk_attrs) == 1 else tuple(row) for row in rows]
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
where = (
|
||||||
|
pk_attrs[0].in_(ids) if len(pk_attrs) == 1 else tuple_(*pk_attrs).in_(ids)
|
||||||
|
)
|
||||||
|
item_q = select(cls.model).where(where)
|
||||||
|
if resolved := cls._resolve_load_options(load_options):
|
||||||
|
item_q = item_q.options(*resolved)
|
||||||
|
item_q = _apply_for_update(item_q, with_for_update)
|
||||||
|
found = (await session.execute(item_q)).unique().scalars().all()
|
||||||
|
|
||||||
|
rank = {pk: n for n, pk in enumerate(ids)}
|
||||||
|
|
||||||
|
def _key(obj: Any) -> Any:
|
||||||
|
values = tuple(getattr(obj, a.key) for a in pk_attrs)
|
||||||
|
return values[0] if len(pk_attrs) == 1 else values
|
||||||
|
|
||||||
|
return cast(
|
||||||
|
list[ModelType], sorted(found, key=lambda o: rank.get(_key(o), len(ids)))
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _resolve_load_options(
|
def _resolve_load_options(
|
||||||
cls, load_options: Sequence[ExecutableOption] | None
|
cls, load_options: Sequence[ExecutableOption] | None
|
||||||
@@ -173,30 +270,17 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
return cls.default_load_options
|
return cls.default_load_options
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _capture_pk_values(cls: type[Self], instance: ModelType) -> dict[str, Any]:
|
async def _reload_with_options(
|
||||||
"""Capture PK values off instance — call before commit expires attributes."""
|
cls: type[Self], session: AsyncSession, instance: DeclarativeBase
|
||||||
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:
|
) -> ModelType:
|
||||||
"""Re-query by previously captured PK values, with default_load_options applied."""
|
"""Re-query instance by PK with default_load_options applied."""
|
||||||
# Only called when cls.default_load_options is set (see call sites).
|
mapper = cls.model.__mapper__
|
||||||
pk_filters = [
|
pk_filters = [
|
||||||
getattr(cls.model, key) == value for key, value in pk_values.items()
|
getattr(cls.model, cast(str, col.key))
|
||||||
|
== getattr(instance, cast(str, col.key))
|
||||||
|
for col in mapper.primary_key
|
||||||
]
|
]
|
||||||
q = select(cls.model).where(and_(*pk_filters))
|
return await cls.get(session, filters=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
|
@classmethod
|
||||||
async def _resolve_m2m(
|
async def _resolve_m2m(
|
||||||
@@ -302,13 +386,29 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
own_filters=own_filters,
|
own_filters=own_filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_search_fields(
|
||||||
|
cls: type[Self],
|
||||||
|
search_fields: Sequence[SearchFieldType] | None,
|
||||||
|
) -> Sequence[SearchFieldType] | None:
|
||||||
|
"""Return search_fields if given, otherwise fall back to the class-level default."""
|
||||||
|
return search_fields if search_fields is not None else cls.searchable_fields
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_order_fields(
|
||||||
|
cls: type[Self],
|
||||||
|
order_fields: Sequence[OrderFieldType] | None,
|
||||||
|
) -> Sequence[OrderFieldType] | None:
|
||||||
|
"""Return order_fields if given, otherwise fall back to the class-level default."""
|
||||||
|
return order_fields if order_fields is not None else cls.order_fields
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _resolve_search_columns(
|
def _resolve_search_columns(
|
||||||
cls: type[Self],
|
cls: type[Self],
|
||||||
search_fields: Sequence[SearchFieldType] | None,
|
search_fields: Sequence[SearchFieldType] | None,
|
||||||
) -> list[str] | None:
|
) -> list[str] | None:
|
||||||
"""Return search column keys, or None if no searchable fields configured."""
|
"""Return search column keys, or None if no searchable fields configured."""
|
||||||
fields = search_fields if search_fields is not None else cls.searchable_fields
|
fields = cls._resolve_search_fields(search_fields)
|
||||||
if not fields:
|
if not fields:
|
||||||
return None
|
return None
|
||||||
return search_field_keys(fields)
|
return search_field_keys(fields)
|
||||||
@@ -319,7 +419,7 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
order_fields: Sequence[OrderFieldType] | None,
|
order_fields: Sequence[OrderFieldType] | None,
|
||||||
) -> list[str] | None:
|
) -> list[str] | None:
|
||||||
"""Return sort column keys, or None if no order fields configured."""
|
"""Return sort column keys, or None if no order fields configured."""
|
||||||
fields = order_fields if order_fields is not None else cls.order_fields
|
fields = cls._resolve_order_fields(order_fields)
|
||||||
if not fields:
|
if not fields:
|
||||||
return None
|
return None
|
||||||
return sorted(facet_keys(fields))
|
return sorted(facet_keys(fields))
|
||||||
@@ -398,9 +498,7 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
order_field_map: dict[str, OrderFieldType] | None = None
|
order_field_map: dict[str, OrderFieldType] | None = None
|
||||||
order_valid_keys: list[str] | None = None
|
order_valid_keys: list[str] | None = None
|
||||||
if order:
|
if order:
|
||||||
resolved_order = (
|
resolved_order = cls._resolve_order_fields(order_fields)
|
||||||
order_fields if order_fields is not None else cls.order_fields
|
|
||||||
)
|
|
||||||
if resolved_order:
|
if resolved_order:
|
||||||
keys = facet_keys(resolved_order)
|
keys = facet_keys(resolved_order)
|
||||||
order_field_map = dict(zip(keys, resolved_order))
|
order_field_map = dict(zip(keys, resolved_order))
|
||||||
@@ -426,8 +524,21 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fixed: dict[str, Any] = {
|
||||||
|
**pagination_fixed,
|
||||||
|
"search_fields": (cls._resolve_search_fields(search_fields) or [])
|
||||||
|
if search
|
||||||
|
else [],
|
||||||
|
"facet_fields": (cls._resolve_facet_fields(facet_fields) or [])
|
||||||
|
if filter
|
||||||
|
else [],
|
||||||
|
"order_fields": (cls._resolve_order_fields(order_fields) or [])
|
||||||
|
if order
|
||||||
|
else [],
|
||||||
|
}
|
||||||
|
|
||||||
async def dependency(**kwargs: Any) -> dict[str, Any]:
|
async def dependency(**kwargs: Any) -> dict[str, Any]:
|
||||||
result: dict[str, Any] = dict(pagination_fixed)
|
result: dict[str, Any] = dict(fixed)
|
||||||
for name in pagination_param_names:
|
for name in pagination_param_names:
|
||||||
result[name] = kwargs[name]
|
result[name] = kwargs[name]
|
||||||
|
|
||||||
@@ -750,14 +861,9 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
setattr(db_model, rel_attr, related_instances)
|
setattr(db_model, rel_attr, related_instances)
|
||||||
|
|
||||||
session.add(db_model)
|
session.add(db_model)
|
||||||
pk_values: dict[str, Any] | None = None
|
|
||||||
if cls.default_load_options:
|
|
||||||
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)
|
await session.refresh(db_model)
|
||||||
|
if cls.default_load_options:
|
||||||
|
db_model = await cls._reload_with_options(session, db_model)
|
||||||
result = cast(ModelType, db_model)
|
result = cast(ModelType, db_model)
|
||||||
if schema:
|
if schema:
|
||||||
return Response(data=schema.model_validate(result))
|
return Response(data=schema.model_validate(result))
|
||||||
@@ -1023,9 +1129,21 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
q = q.where(and_(*filters))
|
q = q.where(and_(*filters))
|
||||||
if resolved := cls._resolve_load_options(load_options):
|
if resolved := cls._resolve_load_options(load_options):
|
||||||
q = q.options(*resolved)
|
q = q.options(*resolved)
|
||||||
q = _apply_for_update(q, with_for_update)
|
|
||||||
if order_by is not None:
|
if order_by is not None:
|
||||||
q = q.order_by(order_by)
|
q = q.order_by(order_by)
|
||||||
|
|
||||||
|
if limit is not None and joins:
|
||||||
|
return await cls._page_entities(
|
||||||
|
session,
|
||||||
|
q,
|
||||||
|
order_clauses=[] if order_by is None else [order_by],
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
load_options=load_options,
|
||||||
|
with_for_update=with_for_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
q = _apply_for_update(q, with_for_update)
|
||||||
if offset is not None:
|
if offset is not None:
|
||||||
q = q.offset(offset)
|
q = q.offset(offset)
|
||||||
if limit is not None:
|
if limit is not None:
|
||||||
@@ -1122,15 +1240,9 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
m2m_resolved = await cls._resolve_m2m(session, obj, only_set=True)
|
m2m_resolved = await cls._resolve_m2m(session, obj, only_set=True)
|
||||||
for rel_attr, related_instances in m2m_resolved.items():
|
for rel_attr, related_instances in m2m_resolved.items():
|
||||||
setattr(db_model, rel_attr, related_instances)
|
setattr(db_model, rel_attr, related_instances)
|
||||||
|
|
||||||
pk_values: dict[str, Any] | None = None
|
|
||||||
if cls.default_load_options:
|
|
||||||
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)
|
await session.refresh(db_model)
|
||||||
|
if cls.default_load_options:
|
||||||
|
db_model = await cls._reload_with_options(session, db_model)
|
||||||
if schema:
|
if schema:
|
||||||
return Response(data=schema.model_validate(db_model))
|
return Response(data=schema.model_validate(db_model))
|
||||||
return db_model
|
return db_model
|
||||||
@@ -1374,17 +1486,31 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
q = q.where(and_(*filters))
|
q = q.where(and_(*filters))
|
||||||
if resolved := cls._resolve_load_options(load_options):
|
if resolved := cls._resolve_load_options(load_options):
|
||||||
q = q.options(*resolved)
|
q = q.options(*resolved)
|
||||||
if order_by is not None:
|
order_clauses: list[Any] = [] if order_by is None else [order_by]
|
||||||
q = q.order_by(order_by)
|
q = q.order_by(*order_clauses)
|
||||||
|
|
||||||
|
fetch_limit = items_per_page if include_total else items_per_page + 1
|
||||||
|
total_count: int | None = None
|
||||||
|
# A to-many join repeats each entity, so LIMIT would slice joined rows
|
||||||
|
# and `.unique()` would shrink the page after the fact.
|
||||||
|
if _fans_out(search_joins, order_joins):
|
||||||
|
raw_items = await cls._page_entities(
|
||||||
|
session,
|
||||||
|
q,
|
||||||
|
order_clauses=order_clauses,
|
||||||
|
limit=fetch_limit,
|
||||||
|
offset=offset,
|
||||||
|
load_options=load_options,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = await session.execute(q.offset(offset).limit(fetch_limit))
|
||||||
|
raw_items = cast(list[ModelType], result.unique().scalars().all())
|
||||||
|
fetched = len(raw_items)
|
||||||
|
raw_items = raw_items[:items_per_page]
|
||||||
|
|
||||||
if include_total:
|
if include_total:
|
||||||
q = q.offset(offset).limit(items_per_page)
|
|
||||||
result = await session.execute(q)
|
|
||||||
raw_items = cast(list[ModelType], result.unique().scalars().all())
|
|
||||||
|
|
||||||
# Count query (with same joins and filters)
|
# Count query (with same joins and filters)
|
||||||
pk_col = cls.model.__mapper__.primary_key[0]
|
count_q = select(func.count(func.distinct(cls._pk_attrs()[0])))
|
||||||
count_q = select(func.count(func.distinct(getattr(cls.model, pk_col.name))))
|
|
||||||
count_q = count_q.select_from(cls.model)
|
count_q = count_q.select_from(cls.model)
|
||||||
|
|
||||||
# Apply explicit joins to count query
|
# Apply explicit joins to count query
|
||||||
@@ -1397,16 +1523,11 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
count_q = count_q.where(and_(*filters))
|
count_q = count_q.where(and_(*filters))
|
||||||
|
|
||||||
count_result = await session.execute(count_q)
|
count_result = await session.execute(count_q)
|
||||||
total_count: int = count_result.scalar_one()
|
total_count = count_result.scalar_one()
|
||||||
has_more = page * items_per_page < total_count
|
has_more = page * items_per_page < total_count
|
||||||
else:
|
else:
|
||||||
# Fetch one extra row to detect if a next page exists without COUNT
|
# One extra row was fetched to detect a next page without COUNT
|
||||||
q = q.offset(offset).limit(items_per_page + 1)
|
has_more = fetched > items_per_page
|
||||||
result = await session.execute(q)
|
|
||||||
raw_items = cast(list[ModelType], result.unique().scalars().all())
|
|
||||||
has_more = len(raw_items) > items_per_page
|
|
||||||
raw_items = raw_items[:items_per_page]
|
|
||||||
total_count = None
|
|
||||||
|
|
||||||
items: list[Any] = [schema.model_validate(item) for item in raw_items]
|
items: list[Any] = [schema.model_validate(item) for item in raw_items]
|
||||||
|
|
||||||
@@ -1543,18 +1664,30 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
q = q.options(*resolved)
|
q = q.options(*resolved)
|
||||||
|
|
||||||
# Cursor column is always the primary sort; reverse direction for prev traversal
|
# Cursor column is always the primary sort; reverse direction for prev traversal
|
||||||
if direction is _CursorDirection.PREV:
|
cursor_clause = (
|
||||||
q = q.order_by(cursor_column.desc())
|
cursor_column.desc()
|
||||||
else:
|
if direction is _CursorDirection.PREV
|
||||||
q = q.order_by(cursor_column)
|
else cursor_column
|
||||||
|
)
|
||||||
|
order_clauses: list[Any] = [cursor_clause]
|
||||||
if order_by is not None:
|
if order_by is not None:
|
||||||
q = q.order_by(order_by)
|
order_clauses.append(order_by)
|
||||||
|
q = q.order_by(*order_clauses)
|
||||||
|
|
||||||
# Fetch one extra to detect whether another page exists in this direction
|
# One extra row detects whether another page exists in this direction.
|
||||||
q = q.limit(items_per_page + 1)
|
# Under a to-many join that extra row may be a duplicate of one already
|
||||||
result = await session.execute(q)
|
# on the page, which reads as "no next page" and ends traversal early.
|
||||||
|
if _fans_out(search_joins, order_joins):
|
||||||
|
raw_items = await cls._page_entities(
|
||||||
|
session,
|
||||||
|
q,
|
||||||
|
order_clauses=order_clauses,
|
||||||
|
limit=items_per_page + 1,
|
||||||
|
load_options=load_options,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = await session.execute(q.limit(items_per_page + 1))
|
||||||
raw_items = cast(list[ModelType], result.unique().scalars().all())
|
raw_items = cast(list[ModelType], result.unique().scalars().all())
|
||||||
|
|
||||||
has_more = len(raw_items) > items_per_page
|
has_more = len(raw_items) > items_per_page
|
||||||
items_page = raw_items[:items_per_page]
|
items_page = raw_items[:items_per_page]
|
||||||
|
|
||||||
|
|||||||
@@ -160,9 +160,11 @@ def build_search_filters(
|
|||||||
column = field
|
column = field
|
||||||
|
|
||||||
# Build the filter (cast to String only when needed, to preserve
|
# Build the filter (cast to String only when needed, to preserve
|
||||||
# pg_trgm GIN index usability on already-String columns)
|
# pg_trgm GIN index usability on already-String columns).
|
||||||
column_as_string = (
|
column_as_string = (
|
||||||
column if isinstance(column.type, String) else column.cast(String)
|
column
|
||||||
|
if isinstance(column.type, String) and not isinstance(column.type, Enum)
|
||||||
|
else column.cast(String)
|
||||||
)
|
)
|
||||||
if config.case_sensitive:
|
if config.case_sensitive:
|
||||||
filters.append(column_as_string.like(f"%{query}%"))
|
filters.append(column_as_string.like(f"%{query}%"))
|
||||||
|
|||||||
@@ -66,10 +66,8 @@ class _CommitOnResponseMiddleware:
|
|||||||
|
|
||||||
async def send_wrapper(message: Message) -> None:
|
async def send_wrapper(message: Message) -> None:
|
||||||
if message["type"] == "http.response.start":
|
if message["type"] == "http.response.start":
|
||||||
# ``scope["state"]`` is the same dict ``request.state`` writes
|
|
||||||
# to, so this is the session stashed by the dependency.
|
|
||||||
state = scope.get("state")
|
state = scope.get("state")
|
||||||
session = state.get(self.state_attr) if state else None
|
session = state.pop(self.state_attr, None) if state else None
|
||||||
if session is not None and session.in_transaction():
|
if session is not None and session.in_transaction():
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await send(message)
|
await send(message)
|
||||||
@@ -158,7 +156,6 @@ class Database:
|
|||||||
# Private, per-instance state attribute; cannot collide with another
|
# Private, per-instance state attribute; cannot collide with another
|
||||||
# Database or be mismatched against the middleware.
|
# Database or be mismatched against the middleware.
|
||||||
self._state_attr = f"_ft_db_session_{id(self):x}"
|
self._state_attr = f"_ft_db_session_{id(self):x}"
|
||||||
self._middleware_installed = False
|
|
||||||
self._disposed = False
|
self._disposed = False
|
||||||
|
|
||||||
async def _dispose(self) -> None:
|
async def _dispose(self) -> None:
|
||||||
@@ -206,7 +203,6 @@ class Database:
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
app.add_middleware(_CommitOnResponseMiddleware, state_attr=self._state_attr)
|
app.add_middleware(_CommitOnResponseMiddleware, state_attr=self._state_attr)
|
||||||
self._middleware_installed = True
|
|
||||||
|
|
||||||
inner_lifespan = app.router.lifespan_context
|
inner_lifespan = app.router.lifespan_context
|
||||||
|
|
||||||
@@ -243,10 +239,17 @@ class Database:
|
|||||||
return await UserCrud.get(session, [User.id == user_id])
|
return await UserCrud.get(session, [User.id == user_id])
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
borrowed = getattr(request.state, self._state_attr, None)
|
||||||
|
if borrowed is not None:
|
||||||
|
yield borrowed
|
||||||
|
return
|
||||||
async with self._open() as session:
|
async with self._open() as session:
|
||||||
setattr(request.state, self._state_attr, session)
|
setattr(request.state, self._state_attr, session)
|
||||||
yield session
|
yield session
|
||||||
if not self._middleware_installed and session.in_transaction():
|
if (
|
||||||
|
getattr(request.state, self._state_attr, None) is session
|
||||||
|
and session.in_transaction()
|
||||||
|
):
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import typing
|
import typing
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Sequence
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi.params import Depends as DependsClass
|
from fastapi.params import Depends as DependsClass
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.sql.base import ExecutableOption
|
||||||
|
|
||||||
from .crud import CrudFactory
|
from .crud import AsyncCrud, CrudFactory
|
||||||
from .types import ModelType, SessionDependency
|
from .types import ModelType, SessionDependency
|
||||||
|
|
||||||
__all__ = ["BodyDependency", "PathDependency"]
|
__all__ = ["BodyDependency", "PathDependency"]
|
||||||
@@ -24,12 +25,59 @@ def _unwrap_session_dep(session_dep: SessionDependency) -> Callable[..., Any]:
|
|||||||
return session_dep
|
return session_dep
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_dependency(
|
||||||
|
model: type[ModelType],
|
||||||
|
field: Any,
|
||||||
|
*,
|
||||||
|
session_dep: SessionDependency,
|
||||||
|
param_name: str,
|
||||||
|
crud: type[AsyncCrud[ModelType]] | None,
|
||||||
|
load_options: Sequence[ExecutableOption] | None,
|
||||||
|
) -> ModelType:
|
||||||
|
"""Build a Depends() that fetches one row by ``field == <param_name>``."""
|
||||||
|
session_callable = _unwrap_session_dep(session_dep)
|
||||||
|
if crud is not None and crud.model is not model:
|
||||||
|
raise ValueError(
|
||||||
|
f"crud is bound to {crud.model.__name__}, not {model.__name__}"
|
||||||
|
)
|
||||||
|
crud = crud or CrudFactory(model)
|
||||||
|
|
||||||
|
# `session` has no default here: the __signature__ override below is what
|
||||||
|
# FastAPI reads, and it always passes `session` explicitly.
|
||||||
|
async def dependency(session: AsyncSession, **kwargs: Any) -> ModelType:
|
||||||
|
return await crud.get(
|
||||||
|
session,
|
||||||
|
filters=[field == kwargs[param_name]],
|
||||||
|
load_options=load_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
dependency.__signature__ = inspect.Signature( # ty:ignore[unresolved-attribute]
|
||||||
|
parameters=[
|
||||||
|
inspect.Parameter(
|
||||||
|
param_name,
|
||||||
|
inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
annotation=field.type.python_type,
|
||||||
|
),
|
||||||
|
inspect.Parameter(
|
||||||
|
"session",
|
||||||
|
inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
annotation=AsyncSession,
|
||||||
|
default=Depends(session_callable),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
|
||||||
|
|
||||||
|
|
||||||
def PathDependency(
|
def PathDependency(
|
||||||
model: type[ModelType],
|
model: type[ModelType],
|
||||||
field: Any,
|
field: Any,
|
||||||
*,
|
*,
|
||||||
session_dep: SessionDependency,
|
session_dep: SessionDependency,
|
||||||
param_name: str | None = None,
|
param_name: str | None = None,
|
||||||
|
crud: type[AsyncCrud[ModelType]] | None = None,
|
||||||
|
load_options: Sequence[ExecutableOption] | None = None,
|
||||||
) -> ModelType:
|
) -> ModelType:
|
||||||
"""Create a dependency that fetches a DB object from a path parameter.
|
"""Create a dependency that fetches a DB object from a path parameter.
|
||||||
|
|
||||||
@@ -38,6 +86,10 @@ def PathDependency(
|
|||||||
field: Model field to filter by (e.g., User.id)
|
field: Model field to filter by (e.g., User.id)
|
||||||
session_dep: Session dependency function (e.g., get_db)
|
session_dep: Session dependency function (e.g., get_db)
|
||||||
param_name: Path parameter name (defaults to model_field, e.g., user_id)
|
param_name: Path parameter name (defaults to model_field, e.g., user_id)
|
||||||
|
crud: Existing CRUD class to fetch with, so its ``default_load_options``
|
||||||
|
apply. Defaults to a bare ``CrudFactory(model)``.
|
||||||
|
load_options: SQLAlchemy loader options for the fetch. Overrides the CRUD's
|
||||||
|
``default_load_options`` entirely rather than merging with them.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Depends() instance that resolves to the model instance
|
A Depends() instance that resolves to the model instance
|
||||||
@@ -55,36 +107,14 @@ def PathDependency(
|
|||||||
): ...
|
): ...
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
session_callable = _unwrap_session_dep(session_dep)
|
return _fetch_dependency(
|
||||||
crud = CrudFactory(model)
|
model,
|
||||||
name = (
|
field,
|
||||||
param_name
|
session_dep=session_dep,
|
||||||
if param_name is not None
|
param_name=param_name or f"{model.__name__.lower()}_{field.key}",
|
||||||
else f"{model.__name__.lower()}_{field.key}"
|
crud=crud,
|
||||||
|
load_options=load_options,
|
||||||
)
|
)
|
||||||
python_type = field.type.python_type
|
|
||||||
|
|
||||||
async def dependency(
|
|
||||||
session: AsyncSession = Depends(session_callable), **kwargs: Any
|
|
||||||
) -> ModelType:
|
|
||||||
value = kwargs[name]
|
|
||||||
return await crud.get(session, filters=[field == value])
|
|
||||||
|
|
||||||
dependency.__signature__ = inspect.Signature( # ty:ignore[unresolved-attribute]
|
|
||||||
parameters=[
|
|
||||||
inspect.Parameter(
|
|
||||||
name, inspect.Parameter.KEYWORD_ONLY, annotation=python_type
|
|
||||||
),
|
|
||||||
inspect.Parameter(
|
|
||||||
"session",
|
|
||||||
inspect.Parameter.KEYWORD_ONLY,
|
|
||||||
annotation=AsyncSession,
|
|
||||||
default=Depends(session_callable),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
|
|
||||||
|
|
||||||
|
|
||||||
def BodyDependency(
|
def BodyDependency(
|
||||||
@@ -93,6 +123,8 @@ def BodyDependency(
|
|||||||
*,
|
*,
|
||||||
session_dep: SessionDependency,
|
session_dep: SessionDependency,
|
||||||
body_field: str,
|
body_field: str,
|
||||||
|
crud: type[AsyncCrud[ModelType]] | None = None,
|
||||||
|
load_options: Sequence[ExecutableOption] | None = None,
|
||||||
) -> ModelType:
|
) -> ModelType:
|
||||||
"""Create a dependency that fetches a DB object from a body field.
|
"""Create a dependency that fetches a DB object from a body field.
|
||||||
|
|
||||||
@@ -101,6 +133,10 @@ def BodyDependency(
|
|||||||
field: Model field to filter by (e.g., User.id)
|
field: Model field to filter by (e.g., User.id)
|
||||||
session_dep: Session dependency function (e.g., get_db)
|
session_dep: Session dependency function (e.g., get_db)
|
||||||
body_field: Name of the field in the request body
|
body_field: Name of the field in the request body
|
||||||
|
crud: Existing CRUD class to fetch with, so its ``default_load_options``
|
||||||
|
apply. Defaults to a bare ``CrudFactory(model)``.
|
||||||
|
load_options: SQLAlchemy loader options for the fetch. Overrides the CRUD's
|
||||||
|
``default_load_options`` entirely rather than merging with them.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Depends() instance that resolves to the model instance
|
A Depends() instance that resolves to the model instance
|
||||||
@@ -120,28 +156,11 @@ def BodyDependency(
|
|||||||
): ...
|
): ...
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
session_callable = _unwrap_session_dep(session_dep)
|
return _fetch_dependency(
|
||||||
crud = CrudFactory(model)
|
model,
|
||||||
python_type = field.type.python_type
|
field,
|
||||||
|
session_dep=session_dep,
|
||||||
async def dependency(
|
param_name=body_field,
|
||||||
session: AsyncSession = Depends(session_callable), **kwargs: Any
|
crud=crud,
|
||||||
) -> ModelType:
|
load_options=load_options,
|
||||||
value = kwargs[body_field]
|
|
||||||
return await crud.get(session, filters=[field == value])
|
|
||||||
|
|
||||||
dependency.__signature__ = inspect.Signature( # ty:ignore[unresolved-attribute]
|
|
||||||
parameters=[
|
|
||||||
inspect.Parameter(
|
|
||||||
body_field, inspect.Parameter.KEYWORD_ONLY, annotation=python_type
|
|
||||||
),
|
|
||||||
inspect.Parameter(
|
|
||||||
"session",
|
|
||||||
inspect.Parameter.KEYWORD_ONLY,
|
|
||||||
annotation=AsyncSession,
|
|
||||||
default=Depends(session_callable),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Any
|
|||||||
from sqlalchemy import event, select, tuple_
|
from sqlalchemy import event, select, tuple_
|
||||||
from sqlalchemy import inspect as sa_inspect
|
from sqlalchemy import inspect as sa_inspect
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlalchemy.orm.attributes import set_committed_value as _sa_set_committed_value
|
from sqlalchemy.orm.attributes import set_committed_value as _sa_set_committed_value
|
||||||
|
|
||||||
from ..logger import get_logger
|
from ..logger import get_logger
|
||||||
@@ -189,17 +190,44 @@ async def _invoke_callback(
|
|||||||
await result
|
await result
|
||||||
|
|
||||||
|
|
||||||
|
def _loaded_relationships(obj: Any) -> set[str]:
|
||||||
|
"""Relationship keys currently loaded on *obj*."""
|
||||||
|
state = sa_inspect(obj)
|
||||||
|
unloaded = state.unloaded
|
||||||
|
return {
|
||||||
|
rel.key
|
||||||
|
for rel in state.mapper.relationships
|
||||||
|
if rel.key not in unloaded and rel.lazy not in ("dynamic", "write_only")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_loaded_relationships(session: Any) -> dict[int, set[str]]:
|
||||||
|
"""Record loaded relationships for the tracked objects, keyed by ``id``."""
|
||||||
|
objs = list(session.info.get(_SESSION_CREATES, []))
|
||||||
|
objs += [obj for obj, _ in session.info.get(_SESSION_UPDATES, {}).values()]
|
||||||
|
return {id(obj): _loaded_relationships(obj) for obj in objs}
|
||||||
|
|
||||||
|
|
||||||
async def _batch_reload(
|
async def _batch_reload(
|
||||||
session: AsyncSession, model: type, pk_tuples: list[tuple[Any, ...]]
|
session: AsyncSession,
|
||||||
|
model: type,
|
||||||
|
objs: list[Any],
|
||||||
|
preloaded: dict[int, set[str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Re-populate all rows of *model* identified by *pk_tuples* in one round trip."""
|
"""Re-populate all rows of *model* in one round trip."""
|
||||||
pk_cols = sa_inspect(model, raiseerr=True).primary_key
|
pk_cols = sa_inspect(model, raiseerr=True).primary_key
|
||||||
|
pk_tuples = [sa_inspect(obj).key[1] for obj in objs]
|
||||||
where = (
|
where = (
|
||||||
pk_cols[0].in_([pk[0] for pk in pk_tuples])
|
pk_cols[0].in_([pk[0] for pk in pk_tuples])
|
||||||
if len(pk_cols) == 1
|
if len(pk_cols) == 1
|
||||||
else tuple_(*pk_cols).in_(pk_tuples)
|
else tuple_(*pk_cols).in_(pk_tuples)
|
||||||
)
|
)
|
||||||
q = select(model).where(where).execution_options(populate_existing=True)
|
q = select(model).where(where).execution_options(populate_existing=True)
|
||||||
|
loaded: set[str] = set()
|
||||||
|
for obj in objs:
|
||||||
|
loaded |= preloaded.get(id(obj), set())
|
||||||
|
if loaded:
|
||||||
|
q = q.options(*(selectinload(getattr(model, key)) for key in loaded))
|
||||||
await session.execute(q)
|
await session.execute(q)
|
||||||
|
|
||||||
|
|
||||||
@@ -207,6 +235,7 @@ class EventSession(AsyncSession):
|
|||||||
"""AsyncSession subclass that dispatches lifecycle callbacks after commit."""
|
"""AsyncSession subclass that dispatches lifecycle callbacks after commit."""
|
||||||
|
|
||||||
async def commit(self) -> None:
|
async def commit(self) -> None:
|
||||||
|
preloaded = _snapshot_loaded_relationships(self)
|
||||||
await super().commit()
|
await super().commit()
|
||||||
|
|
||||||
creates: list[Any] = self.info.pop(_SESSION_CREATES, [])
|
creates: list[Any] = self.info.pop(_SESSION_CREATES, [])
|
||||||
@@ -249,25 +278,25 @@ class EventSession(AsyncSession):
|
|||||||
# session.get() per object.
|
# session.get() per object.
|
||||||
create_items: list[Any] = []
|
create_items: list[Any] = []
|
||||||
update_items: list[tuple[Any, dict[str, dict[str, Any]]]] = []
|
update_items: list[tuple[Any, dict[str, dict[str, Any]]]] = []
|
||||||
pk_by_type: dict[type, list[tuple[Any, ...]]] = {}
|
objs_by_type: dict[type, list[Any]] = {}
|
||||||
|
|
||||||
for obj in creates:
|
for obj in creates:
|
||||||
state = sa_inspect(obj, raiseerr=False)
|
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
|
continue
|
||||||
create_items.append(obj)
|
create_items.append(obj)
|
||||||
pk_by_type.setdefault(type(obj), []).append(state.key[1])
|
objs_by_type.setdefault(type(obj), []).append(obj)
|
||||||
|
|
||||||
for obj, changes in field_changes.values():
|
for obj, changes in field_changes.values():
|
||||||
state = sa_inspect(obj, raiseerr=False)
|
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
|
continue
|
||||||
update_items.append((obj, changes))
|
update_items.append((obj, changes))
|
||||||
pk_by_type.setdefault(type(obj), []).append(state.key[1])
|
objs_by_type.setdefault(type(obj), []).append(obj)
|
||||||
|
|
||||||
for model, pk_tuples in pk_by_type.items():
|
for model, objs in objs_by_type.items():
|
||||||
try:
|
try:
|
||||||
await _batch_reload(self, model, pk_tuples)
|
await _batch_reload(self, model, objs, preloaded)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_logger.error(_CALLBACK_ERROR_MSG, exc_info=exc)
|
_logger.error(_CALLBACK_ERROR_MSG, exc_info=exc)
|
||||||
|
|
||||||
|
|||||||
@@ -466,6 +466,30 @@ class TestDefaultLoadOptionsIntegration:
|
|||||||
assert updated.role is not None
|
assert updated.role is not None
|
||||||
assert updated.role.name == "admin"
|
assert updated.role.name == "admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_does_not_expire_already_loaded_relationships(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""create()'s reload must not blow away loaded state on related objects."""
|
||||||
|
UserWithDefaultLoad = CrudFactory(
|
||||||
|
User, default_load_options=[selectinload(User.role)]
|
||||||
|
)
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
role = await RoleCrud.get(
|
||||||
|
db_session,
|
||||||
|
filters=[Role.id == role.id],
|
||||||
|
load_options=[selectinload(Role.users)],
|
||||||
|
)
|
||||||
|
assert role.users == []
|
||||||
|
|
||||||
|
await UserWithDefaultLoad.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="alice", email="alice@test.com", role_id=role.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
# must not trigger a lazy load
|
||||||
|
assert role.users == []
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_load_options_overrides_default_load_options(
|
async def test_load_options_overrides_default_load_options(
|
||||||
self, db_session: AsyncSession
|
self, db_session: AsyncSession
|
||||||
|
|||||||
+350
-1
@@ -5,6 +5,7 @@ import uuid
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlalchemy.sql.elements import ColumnElement, UnaryExpression
|
from sqlalchemy.sql.elements import ColumnElement, UnaryExpression
|
||||||
|
|
||||||
from fastapi_toolsets.crud import (
|
from fastapi_toolsets.crud import (
|
||||||
@@ -16,7 +17,7 @@ from fastapi_toolsets.crud import (
|
|||||||
get_searchable_fields,
|
get_searchable_fields,
|
||||||
)
|
)
|
||||||
from fastapi_toolsets.exceptions import InvalidOrderFieldError
|
from fastapi_toolsets.exceptions import InvalidOrderFieldError
|
||||||
from fastapi_toolsets.schemas import OffsetPagination, PaginationType
|
from fastapi_toolsets.schemas import OffsetPagination, PaginationType, PydanticBase
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
Article,
|
Article,
|
||||||
@@ -29,15 +30,19 @@ from .conftest import (
|
|||||||
OrderCrud,
|
OrderCrud,
|
||||||
OrderRead,
|
OrderRead,
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
|
Post,
|
||||||
|
PostCrud,
|
||||||
Role,
|
Role,
|
||||||
RoleCreate,
|
RoleCreate,
|
||||||
RoleCrud,
|
RoleCrud,
|
||||||
RoleCursorCrud,
|
RoleCursorCrud,
|
||||||
RoleRead,
|
RoleRead,
|
||||||
|
Tag,
|
||||||
User,
|
User,
|
||||||
UserCreate,
|
UserCreate,
|
||||||
UserCrud,
|
UserCrud,
|
||||||
UserRead,
|
UserRead,
|
||||||
|
post_tags,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -388,6 +393,295 @@ class TestBuildSearchFilters:
|
|||||||
|
|
||||||
assert "CAST" in str(filters[0])
|
assert "CAST" in str(filters[0])
|
||||||
|
|
||||||
|
def test_casts_enum_column(self):
|
||||||
|
"""Enum subclasses String but maps to a native DB enum, which has no ILIKE."""
|
||||||
|
from fastapi_toolsets.crud.search import build_search_filters
|
||||||
|
|
||||||
|
filters, _ = build_search_filters(Order, "PEND", search_fields=[Order.status])
|
||||||
|
|
||||||
|
assert "CAST" in str(filters[0])
|
||||||
|
|
||||||
|
|
||||||
|
class _PostTitle(PydanticBase):
|
||||||
|
"""Minimal read schema for the to-many pagination tests."""
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
title: str
|
||||||
|
|
||||||
|
|
||||||
|
class _TagName(PydanticBase):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class _PostWithTags(PydanticBase):
|
||||||
|
"""Serialising `tags` fails unless the relation was eager-loaded."""
|
||||||
|
|
||||||
|
title: str
|
||||||
|
tags: list[_TagName]
|
||||||
|
|
||||||
|
|
||||||
|
PostTagSearchCrud = CrudFactory(
|
||||||
|
Post,
|
||||||
|
searchable_fields=[Post.title, (Post.tags, Tag.name)],
|
||||||
|
cursor_column=Post.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
_POST_COUNT = 10
|
||||||
|
|
||||||
|
|
||||||
|
async def _seed_posts_with_tags(session) -> None:
|
||||||
|
"""10 posts, each with 3 tags whose names all match the search term."""
|
||||||
|
author = await UserCrud.create(
|
||||||
|
session, UserCreate(username="fanout", email="fanout@test.com")
|
||||||
|
)
|
||||||
|
for i in range(_POST_COUNT):
|
||||||
|
tags = [Tag(name=f"shared-{i}-{j}") for j in range(3)]
|
||||||
|
session.add_all(tags)
|
||||||
|
session.add(Post(title=f"post{i:02d}", author_id=author.id, tags=tags))
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPaginateToManyJoin:
|
||||||
|
"""Searching a to-many relationship must not truncate or duplicate pages."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_offset_pages_are_full_and_complete(self, db_session: AsyncSession):
|
||||||
|
"""Every page is full and every post is returned exactly once."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
seen: list[str] = []
|
||||||
|
for page in (1, 2):
|
||||||
|
result = await PostTagSearchCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
page=page,
|
||||||
|
items_per_page=5,
|
||||||
|
search="shared",
|
||||||
|
schema=_PostTitle,
|
||||||
|
)
|
||||||
|
assert result.pagination.total_count == _POST_COUNT
|
||||||
|
assert len(result.data) == 5, f"page {page} came back short"
|
||||||
|
seen += [p.title for p in result.data]
|
||||||
|
|
||||||
|
assert len(set(seen)) == _POST_COUNT, "posts duplicated or missing"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_offset_without_total_reports_has_more(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""``has_more`` counts entities, not joined rows."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
first = await PostTagSearchCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
page=1,
|
||||||
|
items_per_page=5,
|
||||||
|
search="shared",
|
||||||
|
include_total=False,
|
||||||
|
schema=_PostTitle,
|
||||||
|
)
|
||||||
|
assert len(first.data) == 5
|
||||||
|
assert first.pagination.has_more is True
|
||||||
|
|
||||||
|
last = await PostTagSearchCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
page=2,
|
||||||
|
items_per_page=5,
|
||||||
|
search="shared",
|
||||||
|
include_total=False,
|
||||||
|
schema=_PostTitle,
|
||||||
|
)
|
||||||
|
assert len(last.data) == 5
|
||||||
|
assert last.pagination.has_more is False
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_cursor_traverses_every_row(self, db_session: AsyncSession):
|
||||||
|
"""Cursor traversal must not stop early."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
seen: list[str] = []
|
||||||
|
cursor: str | None = None
|
||||||
|
for _ in range(_POST_COUNT):
|
||||||
|
result = await PostTagSearchCrud.cursor_paginate(
|
||||||
|
db_session,
|
||||||
|
cursor=cursor,
|
||||||
|
items_per_page=5,
|
||||||
|
search="shared",
|
||||||
|
schema=_PostTitle,
|
||||||
|
)
|
||||||
|
seen += [p.title for p in result.data]
|
||||||
|
cursor = result.pagination.next_cursor
|
||||||
|
if cursor is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
assert len(seen) == _POST_COUNT, "traversal stopped early or repeated rows"
|
||||||
|
assert len(set(seen)) == _POST_COUNT
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_orders_by_a_to_many_column(self, db_session: AsyncSession):
|
||||||
|
"""Ordering by a related column has to be collapsed to an aggregate."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
result = await PostTagSearchCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
page=1,
|
||||||
|
items_per_page=5,
|
||||||
|
search="shared",
|
||||||
|
order_by=Tag.name.asc(),
|
||||||
|
order_joins=[Post.tags],
|
||||||
|
schema=_PostTitle,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(result.data) == 5
|
||||||
|
assert result.pagination.total_count == _POST_COUNT
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_orders_by_a_bare_to_many_column(self, db_session: AsyncSession):
|
||||||
|
"""An order clause with no explicit direction still needs aggregating."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
result = await PostTagSearchCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
page=1,
|
||||||
|
items_per_page=5,
|
||||||
|
search="shared",
|
||||||
|
order_by=Tag.name,
|
||||||
|
order_joins=[Post.tags],
|
||||||
|
schema=_PostTitle,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(result.data) == 5
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_page_past_the_end_is_empty(self, db_session: AsyncSession):
|
||||||
|
"""No keys on the page means no second query and an empty result."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
result = await PostTagSearchCrud.offset_paginate(
|
||||||
|
db_session, page=99, items_per_page=5, search="shared", schema=_PostTitle
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.data == []
|
||||||
|
assert result.pagination.total_count == _POST_COUNT
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_options_apply_on_the_fan_out_path(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""The entity query still honours loader options."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
result = await PostTagSearchCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
page=1,
|
||||||
|
items_per_page=5,
|
||||||
|
search="shared",
|
||||||
|
load_options=[selectinload(Post.tags)],
|
||||||
|
schema=_PostWithTags,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(result.data) == 5
|
||||||
|
assert all(len(p.tags) == 3 for p in result.data)
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_multi_is_not_truncated_by_a_to_many_join(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""`get_multi` takes a raw join, so cardinality cannot be inspected."""
|
||||||
|
await _seed_posts_with_tags(db_session)
|
||||||
|
|
||||||
|
rows = await PostCrud.get_multi(
|
||||||
|
db_session,
|
||||||
|
joins=[(post_tags, post_tags.c.post_id == Post.id)],
|
||||||
|
outer_join=True,
|
||||||
|
limit=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(rows) == 5
|
||||||
|
assert len({r.id for r in rows}) == 5
|
||||||
|
|
||||||
|
def test_grouped_order_only_aggregates_foreign_columns(self):
|
||||||
|
"""A base-table column is left alone; anything else collapses to min()."""
|
||||||
|
from fastapi_toolsets.crud.factory import _grouped_order
|
||||||
|
|
||||||
|
table = Post.__table__
|
||||||
|
|
||||||
|
assert "min" not in str(_grouped_order(Post.title.desc(), table)).lower()
|
||||||
|
|
||||||
|
asc_on_join = str(_grouped_order(Tag.name.asc(), table))
|
||||||
|
desc_on_join = str(_grouped_order(Tag.name.desc(), table))
|
||||||
|
assert "min" in asc_on_join.lower() and asc_on_join.endswith("ASC")
|
||||||
|
assert "min" in desc_on_join.lower() and desc_on_join.endswith("DESC")
|
||||||
|
|
||||||
|
def test_to_one_join_does_not_take_the_fan_out_path(self):
|
||||||
|
"""A to-one join keeps the single-query path."""
|
||||||
|
from fastapi_toolsets.crud.factory import _fans_out
|
||||||
|
|
||||||
|
assert _fans_out([User.role], None) is False
|
||||||
|
assert _fans_out([Post.tags], None) is True
|
||||||
|
assert _fans_out(None, [Post.tags]) is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestSearchEnumColumn:
|
||||||
|
"""Searching an enum column must reach the database, not just build SQL."""
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_search_int_backed_enum(self, db_session: AsyncSession):
|
||||||
|
"""Enum(int, Enum) stores names, so the cast makes 'PEND' match PENDING."""
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="a", status=OrderStatus.PENDING)
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="b", status=OrderStatus.SHIPPED)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
search="PEND",
|
||||||
|
search_fields=[Order.status],
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.pagination.total_count == 1
|
||||||
|
assert result.data[0].status is OrderStatus.PENDING
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_search_str_backed_enum(self, db_session: AsyncSession):
|
||||||
|
"""Same for Enum(str, Enum) — still a native DB enum, still needs the cast."""
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(name="a", status=OrderStatus.PENDING, color=Color.BLUE),
|
||||||
|
)
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session,
|
||||||
|
OrderCreate(name="b", status=OrderStatus.PENDING, color=Color.RED),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
search="BLU",
|
||||||
|
search_fields=[Order.color],
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.pagination.total_count == 1
|
||||||
|
assert result.data[0].color is Color.BLUE
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_search_mixed_enum_and_string_columns(self, db_session: AsyncSession):
|
||||||
|
"""An enum column alongside a plain String column (the get_searchable_fields shape)."""
|
||||||
|
await OrderCrud.create(
|
||||||
|
db_session, OrderCreate(name="widget", status=OrderStatus.SHIPPED)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await OrderCrud.offset_paginate(
|
||||||
|
db_session,
|
||||||
|
search="widget",
|
||||||
|
search_fields=[Order.name, Order.status, Order.color],
|
||||||
|
schema=OrderRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.pagination.total_count == 1
|
||||||
|
|
||||||
|
|
||||||
class TestSearchConfig:
|
class TestSearchConfig:
|
||||||
"""Tests for SearchConfig options."""
|
"""Tests for SearchConfig options."""
|
||||||
@@ -2249,6 +2543,16 @@ class TestOrderParamsViaConsolidated:
|
|||||||
assert len(result.data) == 2
|
assert len(result.data) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def _fully_declared_user_crud():
|
||||||
|
"""A CRUD class declaring all three field sets, as a real app would."""
|
||||||
|
return CrudFactory(
|
||||||
|
User,
|
||||||
|
searchable_fields=[User.username],
|
||||||
|
facet_fields=[User.email],
|
||||||
|
order_fields=[User.username],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestOffsetPaginateParamsSchema:
|
class TestOffsetPaginateParamsSchema:
|
||||||
"""Tests for AsyncCrud.offset_paginate_params()."""
|
"""Tests for AsyncCrud.offset_paginate_params()."""
|
||||||
|
|
||||||
@@ -2318,6 +2622,9 @@ class TestOffsetPaginateParamsSchema:
|
|||||||
"items_per_page": 10,
|
"items_per_page": 10,
|
||||||
"include_total": False,
|
"include_total": False,
|
||||||
"include_facets": True,
|
"include_facets": True,
|
||||||
|
"search_fields": [],
|
||||||
|
"facet_fields": [],
|
||||||
|
"order_fields": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -2361,6 +2668,42 @@ class TestOffsetPaginateParamsSchema:
|
|||||||
assert "search" not in param_names
|
assert "search" not in param_names
|
||||||
assert "search_column" not in param_names
|
assert "search_column" not in param_names
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"kwargs",
|
||||||
|
[
|
||||||
|
{"search": False, "filter": False, "order": False},
|
||||||
|
{"search_fields": [], "facet_fields": [], "order_fields": []},
|
||||||
|
],
|
||||||
|
ids=["flags", "empty-overrides"],
|
||||||
|
)
|
||||||
|
async def test_disabled_features_clear_response_metadata(
|
||||||
|
self, db_session: AsyncSession, kwargs
|
||||||
|
):
|
||||||
|
"""Disabling a feature on one endpoint also drops it from the response."""
|
||||||
|
await UserCrud.create(db_session, UserCreate(username="bob", email="b@x.io"))
|
||||||
|
Crud = _fully_declared_user_crud()
|
||||||
|
dep = Crud.offset_paginate_params(**kwargs)
|
||||||
|
params = await dep(page=1, items_per_page=10)
|
||||||
|
result = await Crud.offset_paginate(db_session, **params, schema=UserRead)
|
||||||
|
assert result.search_columns is None
|
||||||
|
assert result.order_columns is None
|
||||||
|
assert result.filter_attributes is None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_enabled_features_keep_response_metadata(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""The declared class defaults still reach the response when left enabled."""
|
||||||
|
await UserCrud.create(db_session, UserCreate(username="bob", email="b@x.io"))
|
||||||
|
Crud = _fully_declared_user_crud()
|
||||||
|
dep = Crud.offset_paginate_params()
|
||||||
|
params = await dep(page=1, items_per_page=10)
|
||||||
|
result = await Crud.offset_paginate(db_session, **params, schema=UserRead)
|
||||||
|
assert result.search_columns == ["id", "username"]
|
||||||
|
assert result.order_columns == ["username"]
|
||||||
|
assert result.filter_attributes == {"email": ["b@x.io"]}
|
||||||
|
|
||||||
def test_filter_enabled_but_no_facet_fields(self):
|
def test_filter_enabled_but_no_facet_fields(self):
|
||||||
"""filter=True with no facet_fields silently skips filter params."""
|
"""filter=True with no facet_fields silently skips filter params."""
|
||||||
dep = RoleCrud.offset_paginate_params(search=False, filter=True, order=False)
|
dep = RoleCrud.offset_paginate_params(search=False, filter=True, order=False)
|
||||||
@@ -2433,6 +2776,9 @@ class TestCursorPaginateParamsSchema:
|
|||||||
"cursor": None,
|
"cursor": None,
|
||||||
"items_per_page": 5,
|
"items_per_page": 5,
|
||||||
"include_facets": True,
|
"include_facets": True,
|
||||||
|
"search_fields": [],
|
||||||
|
"facet_fields": [],
|
||||||
|
"order_fields": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -2543,6 +2889,9 @@ class TestPaginateParamsSchema:
|
|||||||
"items_per_page": 10,
|
"items_per_page": 10,
|
||||||
"include_total": True,
|
"include_total": True,
|
||||||
"include_facets": True,
|
"include_facets": True,
|
||||||
|
"search_fields": [],
|
||||||
|
"facet_fields": [],
|
||||||
|
"order_fields": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|||||||
+128
-9
@@ -6,7 +6,7 @@ from contextlib import asynccontextmanager
|
|||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi import Depends, FastAPI
|
from fastapi import Depends, FastAPI, Security
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
from pydantic import PostgresDsn
|
from pydantic import PostgresDsn
|
||||||
@@ -287,23 +287,37 @@ class TestDatabaseDependency:
|
|||||||
break
|
break
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_skips_commit_when_middleware_installed(self, engine, session_maker):
|
async def test_second_resolution_borrows_session(self, engine):
|
||||||
"""With ``install()``, the dependency must NOT commit — the middleware owns it.
|
"""A second ``Depends(db)`` in one request reuses the stashed session."""
|
||||||
|
db = Database(engine=engine)
|
||||||
|
request = _make_request()
|
||||||
|
|
||||||
Here no middleware actually runs (we call the dependency directly), so the
|
owner_gen = db(request)
|
||||||
open transaction is rolled back on session close and nothing persists.
|
owner = await anext(owner_gen)
|
||||||
"""
|
borrower_gen = db(request)
|
||||||
|
assert await anext(borrower_gen) is owner
|
||||||
|
|
||||||
|
with pytest.raises(StopAsyncIteration): # teardown runs borrower-first
|
||||||
|
await anext(borrower_gen)
|
||||||
|
assert owner.in_transaction() # the borrower must not close what it borrowed
|
||||||
|
|
||||||
|
with pytest.raises(StopAsyncIteration):
|
||||||
|
await anext(owner_gen)
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_commits_when_middleware_did_not_run(self, engine, session_maker):
|
||||||
|
"""``install()`` is per-``Database``, but the commit is per-request."""
|
||||||
db = Database(engine=engine)
|
db = Database(engine=engine)
|
||||||
db.install(FastAPI())
|
db.install(FastAPI())
|
||||||
|
|
||||||
async for session in db(_make_request()):
|
async for session in db(_make_request()):
|
||||||
role = Role(name="mw_owns_commit")
|
role = Role(name="mw_never_ran")
|
||||||
session.add(role)
|
session.add(role)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
async with session_maker() as verify:
|
async with session_maker() as verify:
|
||||||
result = await RoleCrud.first(verify, [Role.name == "mw_owns_commit"])
|
result = await RoleCrud.first(verify, [Role.name == "mw_never_ran"])
|
||||||
assert result is None
|
assert result is not None
|
||||||
|
|
||||||
|
|
||||||
class TestDatabaseSession:
|
class TestDatabaseSession:
|
||||||
@@ -1523,6 +1537,55 @@ def _build_app(db: Database) -> FastAPI:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return {"id": str(role.id), "name": role.name}
|
return {"id": str(role.id), "name": role.name}
|
||||||
|
|
||||||
|
async def _scoped_writer(
|
||||||
|
body: RoleCreate, session: AsyncSession = Security(db, scopes=["roles:write"])
|
||||||
|
) -> int:
|
||||||
|
# Security scopes give this a different dependency cache key than the
|
||||||
|
# endpoint's plain ``Depends(db)``. Without borrowing it opens a second
|
||||||
|
# session, and whichever one the middleware does not hold is discarded.
|
||||||
|
await RoleCrud.create(session, RoleCreate(name=f"{body.name}_sub"))
|
||||||
|
return id(session)
|
||||||
|
|
||||||
|
@app.post("/roles-two-cache-keys")
|
||||||
|
async def create_via_two_cache_keys(
|
||||||
|
body: RoleCreate,
|
||||||
|
sub_session_id: int = Depends(_scoped_writer),
|
||||||
|
session: AsyncSession = Depends(db),
|
||||||
|
) -> dict:
|
||||||
|
await RoleCrud.create(session, body)
|
||||||
|
return {"same_session": sub_session_id == id(session)}
|
||||||
|
|
||||||
|
async def _fn_writer(
|
||||||
|
body: RoleCreate, session: AsyncSession = Depends(db, scope="function")
|
||||||
|
) -> None:
|
||||||
|
# ``scope="function"`` unwinds before the response is sent, taking the
|
||||||
|
# session with it — so the commit cannot be left to the middleware.
|
||||||
|
await RoleCrud.create(session, RoleCreate(name=f"{body.name}_fn"))
|
||||||
|
|
||||||
|
@app.post("/roles-function-scope")
|
||||||
|
async def create_with_function_scope(
|
||||||
|
body: RoleCreate,
|
||||||
|
boom: bool = False,
|
||||||
|
_: None = Depends(_fn_writer),
|
||||||
|
session: AsyncSession = Depends(db),
|
||||||
|
) -> dict:
|
||||||
|
await RoleCrud.create(session, body)
|
||||||
|
if boom:
|
||||||
|
raise RuntimeError("boom after write")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
@app.post("/roles-function-scope-borrower")
|
||||||
|
async def function_scope_borrows(
|
||||||
|
body: RoleCreate,
|
||||||
|
session: AsyncSession = Depends(db),
|
||||||
|
_: None = Depends(_fn_writer),
|
||||||
|
) -> dict:
|
||||||
|
# Flipped order: the request-scoped dependency owns the session and the
|
||||||
|
# function-scoped one borrows it. The borrower unwinds early but must not
|
||||||
|
# commit or close — the commit still belongs to the middleware.
|
||||||
|
await RoleCrud.create(session, body)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
@app.get("/roles-stream/{name}")
|
@app.get("/roles-stream/{name}")
|
||||||
async def stream_role(
|
async def stream_role(
|
||||||
name: str, session: AsyncSession = Depends(db)
|
name: str, session: AsyncSession = Depends(db)
|
||||||
@@ -1619,6 +1682,62 @@ class TestCommitIntegration:
|
|||||||
# The write made before the stream began is durably committed.
|
# The write made before the stream began is durably committed.
|
||||||
assert await _row_exists(session_maker, "streamed_role")
|
assert await _row_exists(session_maker, "streamed_role")
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_two_cache_keys_share_one_session(self, engine, session_maker):
|
||||||
|
"""Two resolutions of ``Depends(db)`` in one request must share a session."""
|
||||||
|
app = _build_app(Database(engine=engine))
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
resp = await client.post("/roles-two-cache-keys", json={"name": "two_keys"})
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["same_session"] is True
|
||||||
|
assert await _row_exists(session_maker, "two_keys")
|
||||||
|
assert await _row_exists(session_maker, "two_keys_sub")
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_function_scope_commits_before_response(self, engine, session_maker):
|
||||||
|
"""``scope="function"`` unwinds before response-start, so the dependency
|
||||||
|
commits on its way out instead of leaving it to the middleware."""
|
||||||
|
app = _build_app(Database(engine=engine))
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
resp = await client.post("/roles-function-scope", json={"name": "fn_scope"})
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert await _row_exists(session_maker, "fn_scope")
|
||||||
|
assert await _row_exists(session_maker, "fn_scope_fn")
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_function_scope_borrower_leaves_commit_to_middleware(
|
||||||
|
self, engine, session_maker
|
||||||
|
):
|
||||||
|
"""A function-scoped *borrower* unwinds early but owns nothing."""
|
||||||
|
app = _build_app(Database(engine=engine))
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
resp = await client.post(
|
||||||
|
"/roles-function-scope-borrower", json={"name": "fn_borrow"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert await _row_exists(session_maker, "fn_borrow")
|
||||||
|
assert await _row_exists(session_maker, "fn_borrow_fn")
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_function_scope_error_rolls_back(self, engine, session_maker):
|
||||||
|
"""The early commit must still not fire when the request fails."""
|
||||||
|
app = _build_app(Database(engine=engine))
|
||||||
|
transport = ASGITransport(app=app, raise_app_exceptions=False)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
resp = await client.post(
|
||||||
|
"/roles-function-scope?boom=true", json={"name": "fn_ghost"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 500
|
||||||
|
assert not await _row_exists(session_maker, "fn_ghost")
|
||||||
|
assert not await _row_exists(session_maker, "fn_ghost_fn")
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_multi_write_atomicity(self, engine, session_maker):
|
async def test_multi_write_atomicity(self, engine, session_maker):
|
||||||
"""When the 2nd write fails, the 1st must roll back too (one txn)."""
|
"""When the 2nd write fails, the 1st must roll back too (one txn)."""
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ from typing import Annotated, Any, cast
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
|
from sqlalchemy import inspect as sa_inspect
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from fastapi_toolsets.crud import CrudFactory
|
||||||
from fastapi_toolsets.dependencies import (
|
from fastapi_toolsets.dependencies import (
|
||||||
BodyDependency,
|
BodyDependency,
|
||||||
PathDependency,
|
PathDependency,
|
||||||
_unwrap_session_dep,
|
_unwrap_session_dep,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .conftest import Role, RoleCreate, RoleCrud, User
|
from .conftest import Role, RoleCreate, RoleCrud, User, UserCreate, UserCrud
|
||||||
|
|
||||||
|
|
||||||
async def mock_get_db() -> AsyncGenerator[AsyncSession, None]:
|
async def mock_get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
@@ -275,3 +278,78 @@ class TestBodyDependency:
|
|||||||
|
|
||||||
assert result.id == role.id
|
assert result.id == role.id
|
||||||
assert result.name == "body_annotated_role"
|
assert result.name == "body_annotated_role"
|
||||||
|
|
||||||
|
|
||||||
|
class TestDependencyLoadOptions:
|
||||||
|
"""Both factories can eager-load relations instead of using a bare CRUD."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _make_user(db_session):
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="load_opts_role"))
|
||||||
|
user = await UserCrud.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="load_opts", email="load@opts", role_id=role.id),
|
||||||
|
)
|
||||||
|
db_session.expunge_all()
|
||||||
|
return user
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_bare_crud_leaves_relation_unloaded(self, db_session):
|
||||||
|
"""Baseline: without options the relation is not loaded (what the ticket reports)."""
|
||||||
|
user = await self._make_user(db_session)
|
||||||
|
|
||||||
|
dep = cast(Any, PathDependency(User, User.id, session_dep=mock_get_db))
|
||||||
|
result = await dep.dependency(session=db_session, user_id=user.id)
|
||||||
|
|
||||||
|
assert "role" in sa_inspect(result).unloaded
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_load_options_and_crud_eager_load(self, db_session):
|
||||||
|
"""Every way of asking for eager loading, on both factories, actually loads."""
|
||||||
|
user = await self._make_user(db_session)
|
||||||
|
eager = [selectinload(User.role)]
|
||||||
|
eager_crud = CrudFactory(User, default_load_options=eager)
|
||||||
|
|
||||||
|
deps = {
|
||||||
|
"path/load_options": PathDependency(
|
||||||
|
User, User.id, session_dep=mock_get_db, load_options=eager
|
||||||
|
),
|
||||||
|
"path/crud": PathDependency(
|
||||||
|
User, User.id, session_dep=mock_get_db, crud=eager_crud
|
||||||
|
),
|
||||||
|
"body/load_options": BodyDependency(
|
||||||
|
User,
|
||||||
|
User.id,
|
||||||
|
session_dep=mock_get_db,
|
||||||
|
body_field="user_id",
|
||||||
|
load_options=eager,
|
||||||
|
),
|
||||||
|
"body/crud": BodyDependency(
|
||||||
|
User,
|
||||||
|
User.id,
|
||||||
|
session_dep=mock_get_db,
|
||||||
|
body_field="user_id",
|
||||||
|
crud=eager_crud,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
for label, dep in deps.items():
|
||||||
|
# Drop the identity map, or the next fetch reuses the already-loaded
|
||||||
|
# instance and the assertion passes for the wrong reason.
|
||||||
|
db_session.expunge_all()
|
||||||
|
result = await cast(Any, dep).dependency(
|
||||||
|
session=db_session, user_id=user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "role" not in sa_inspect(result).unloaded, label
|
||||||
|
assert result.role.name == "load_opts_role", label
|
||||||
|
|
||||||
|
def test_crud_bound_to_another_model_is_rejected(self):
|
||||||
|
"""A crud= for a different model would silently query the wrong table.
|
||||||
|
|
||||||
|
``ty`` rejects this statically; the runtime guard covers untyped callers.
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError, match="bound to Role, not User"):
|
||||||
|
PathDependency(
|
||||||
|
User, User.id, session_dep=mock_get_db, crud=cast(Any, RoleCrud)
|
||||||
|
)
|
||||||
|
|||||||
+90
-6
@@ -6,9 +6,16 @@ from types import SimpleNamespace
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import String
|
from sqlalchemy import ForeignKey, String, select
|
||||||
|
from sqlalchemy import inspect as sa_inspect
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
from sqlalchemy.orm import (
|
||||||
|
DeclarativeBase,
|
||||||
|
Mapped,
|
||||||
|
mapped_column,
|
||||||
|
relationship,
|
||||||
|
selectinload,
|
||||||
|
)
|
||||||
|
|
||||||
import fastapi_toolsets.models.watched as _watched_module
|
import fastapi_toolsets.models.watched as _watched_module
|
||||||
from fastapi_toolsets.models import (
|
from fastapi_toolsets.models import (
|
||||||
@@ -107,6 +114,27 @@ async def _watched_on_update(obj, event_type, changes):
|
|||||||
_test_events.append({"event": "update", "obj_id": obj.id, "changes": changes})
|
_test_events.append({"event": "update", "obj_id": obj.id, "changes": changes})
|
||||||
|
|
||||||
|
|
||||||
|
class RelTarget(MixinBase, UUIDMixin):
|
||||||
|
__tablename__ = "mixin_rel_targets"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(50))
|
||||||
|
|
||||||
|
|
||||||
|
class RelOwner(MixinBase, UUIDMixin):
|
||||||
|
"""Watched model with a relationship, to check eager loads survive commit."""
|
||||||
|
|
||||||
|
__tablename__ = "mixin_rel_owners"
|
||||||
|
|
||||||
|
title: Mapped[str] = mapped_column(String(50))
|
||||||
|
target_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("mixin_rel_targets.id"))
|
||||||
|
target: Mapped[RelTarget] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
@listens_for(RelOwner, [ModelEvent.CREATE, ModelEvent.UPDATE])
|
||||||
|
async def _rel_owner_handler(obj, event_type, changes):
|
||||||
|
_test_events.append({"event": event_type.value, "obj_id": obj.id})
|
||||||
|
|
||||||
|
|
||||||
class WatchAllModel(MixinBase, UUIDMixin):
|
class WatchAllModel(MixinBase, UUIDMixin):
|
||||||
"""Model without __watched_fields__ — watches all mapped fields by default."""
|
"""Model without __watched_fields__ — watches all mapped fields by default."""
|
||||||
|
|
||||||
@@ -355,6 +383,62 @@ async def mixin_session_maker():
|
|||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEventSessionPreservesEagerLoads:
|
||||||
|
"""EventSession.commit() must not discard relations an eager load populated."""
|
||||||
|
|
||||||
|
async def _seed_eager(self, session):
|
||||||
|
target = RelTarget(name="t")
|
||||||
|
session.add(target)
|
||||||
|
await session.flush()
|
||||||
|
owner = RelOwner(title="o", target_id=target.id)
|
||||||
|
session.add(owner)
|
||||||
|
await session.flush()
|
||||||
|
loaded = (
|
||||||
|
await session.execute(
|
||||||
|
select(RelOwner)
|
||||||
|
.where(RelOwner.id == owner.id)
|
||||||
|
.options(selectinload(RelOwner.target))
|
||||||
|
)
|
||||||
|
).scalar_one()
|
||||||
|
assert "target" not in sa_inspect(loaded).unloaded
|
||||||
|
return loaded
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_eager_load_survives_commit(self, mixin_session):
|
||||||
|
"""expire_on_commit=False: the reload must not expire the relation."""
|
||||||
|
owner = await self._seed_eager(mixin_session)
|
||||||
|
|
||||||
|
await mixin_session.commit()
|
||||||
|
|
||||||
|
assert "target" not in sa_inspect(owner).unloaded
|
||||||
|
assert owner.target.name == "t"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_eager_load_survives_commit_expire_on_commit(
|
||||||
|
self, mixin_session_expire
|
||||||
|
):
|
||||||
|
"""expire_on_commit=True: what was loaded must be recorded before the commit."""
|
||||||
|
owner = await self._seed_eager(mixin_session_expire)
|
||||||
|
|
||||||
|
await mixin_session_expire.commit()
|
||||||
|
|
||||||
|
assert "target" not in sa_inspect(owner).unloaded
|
||||||
|
assert owner.target.name == "t"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_unloaded_relation_stays_unloaded(self, mixin_session):
|
||||||
|
"""Only what was loaded is restored: the reload must not eager-load extra."""
|
||||||
|
target = RelTarget(name="t")
|
||||||
|
mixin_session.add(target)
|
||||||
|
await mixin_session.flush()
|
||||||
|
owner = RelOwner(title="o", target_id=target.id)
|
||||||
|
mixin_session.add(owner)
|
||||||
|
|
||||||
|
await mixin_session.commit()
|
||||||
|
|
||||||
|
assert "target" in sa_inspect(owner).unloaded
|
||||||
|
|
||||||
|
|
||||||
class TestUUIDMixin:
|
class TestUUIDMixin:
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_uuid_generated_by_db(self, mixin_session):
|
async def test_uuid_generated_by_db(self, mixin_session):
|
||||||
@@ -1013,10 +1097,10 @@ class TestEventCallbacks:
|
|||||||
|
|
||||||
real_batch_reload = _watched_module._batch_reload
|
real_batch_reload = _watched_module._batch_reload
|
||||||
|
|
||||||
async def racing_batch_reload(session, model, pk_tuples):
|
async def racing_batch_reload(session, model, objs, preloaded):
|
||||||
if any(pk[0] == doomed_id for pk in pk_tuples):
|
if any(getattr(o, "id", None) == doomed_id for o in objs):
|
||||||
await kill_doomed_row_once()
|
await kill_doomed_row_once()
|
||||||
return await real_batch_reload(session, model, pk_tuples)
|
return await real_batch_reload(session, model, objs, preloaded)
|
||||||
|
|
||||||
# Patch the batched reload EventSession.commit() uses to pick up
|
# Patch the batched reload EventSession.commit() uses to pick up
|
||||||
# server defaults, so this test still exercises the race.
|
# server defaults, so this test still exercises the race.
|
||||||
@@ -1039,7 +1123,7 @@ class TestEventCallbacks:
|
|||||||
obj = WatchedModel(status="active", other="x")
|
obj = WatchedModel(status="active", other="x")
|
||||||
mixin_session.add(obj)
|
mixin_session.add(obj)
|
||||||
|
|
||||||
async def failing_batch_reload(session, model, pk_tuples):
|
async def failing_batch_reload(session, model, objs, preloaded):
|
||||||
raise RuntimeError("reload failed")
|
raise RuntimeError("reload failed")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.139.0"
|
version = "0.141.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-doc" },
|
{ name = "annotated-doc" },
|
||||||
@@ -308,14 +308,14 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-toolsets"
|
name = "fastapi-toolsets"
|
||||||
version = "5.1.0"
|
version = "5.1.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asyncpg" },
|
{ name = "asyncpg" },
|
||||||
@@ -814,40 +814,40 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prek"
|
name = "prek"
|
||||||
version = "0.4.9"
|
version = "0.4.14"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/cf/51/135dc6ba2c021ce32b40700c8c337db72d802893e15291f4b3056076582f/prek-0.4.14.tar.gz", hash = "sha256:f6d0952e31ffd6e508660749dd51b8d8de96e955ed12c40e411f3224f502fed2", size = 537668, upload-time = "2026-08-17T04:27:55.031Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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/9d/75/727724174d419cab6e11e0c22c3fbcdd083312b0a0202dfa6befdb7f1bcb/prek-0.4.14-py3-none-linux_armv6l.whl", hash = "sha256:cf7fe2e07c99948ca3326fbd4254054cf4caa71a69cf9032be09c3938544e1e3", size = 5878406, upload-time = "2026-08-17T04:27:30.135Z" },
|
||||||
{ 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/34/b6/82dfb41347342b4c53c1ddd27cfa81d3e1cf4ad0d5d76493c51e8da58dc9/prek-0.4.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6c8043aa5555c2ada561f4c69429e5117f370ad92f7d708340613b0965e725bd", size = 6216669, upload-time = "2026-08-17T04:27:31.795Z" },
|
||||||
{ 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/5b/68/ade0ba8b8c0044a3f7ca1a5cd7c97bc10656398c67466b04235f0f0b7418/prek-0.4.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:355570f0d8366a56817e55f44edf736ffc9ff2323894e21cd739ab519d343541", size = 5728085, upload-time = "2026-08-17T04:27:33.322Z" },
|
||||||
{ 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/a0/cc/8ddb67fb000d1ad4c95288e6c7e27989d3c172cd88893e3a6f9cdd7199fb/prek-0.4.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:334f48a2b19e63c5ab741e601cb6088caaa11d1b75146900dc298b055452d551", size = 6043650, upload-time = "2026-08-17T04:27:34.699Z" },
|
||||||
{ 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/5b/49/2780489798147fa1e81fa017af384a40229802cf36680cf72c74478d143c/prek-0.4.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5fbb17270df2dcb3c1aa6edfaa68f850d7968862413e58b1572e41c981f644f9", size = 5786094, upload-time = "2026-08-17T04:27:36.282Z" },
|
||||||
{ 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/6a/2a/27a4cdfa767b46663eb32a2b5f78ebaaeefc6e5caa87a21587216a504e31/prek-0.4.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ec46be0b1b45943a0746fdf69feeede274b0230bad32b11e2489bb600846081", size = 6239874, upload-time = "2026-08-17T04:27:37.984Z" },
|
||||||
{ 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/e0/ca/5ae58d95bc9dbad75ea13aea83fe0acfaca37d027c6bd7f9f1d1d97f3666/prek-0.4.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a21eea6ee996dbba351c1af8c6bd1959a61c37949757c0b97aef3edde82c126a", size = 6968009, upload-time = "2026-08-17T04:27:39.48Z" },
|
||||||
{ 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/bb/1c/c6a7800406f987559fc8e7c2cfd257a5a68096e806e953f143b488e00e4f/prek-0.4.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34169cb1c8dfbe4b9cb7af164f5c1c3dff68929c61ca1e1de28d378b32720836", size = 6443370, upload-time = "2026-08-17T04:27:41.181Z" },
|
||||||
{ 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/92/38/4bf84223216ce2d31b500691d1644a9f35d6e468cb8ecb9a7dfbc66bc82c/prek-0.4.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e39772ebf579f957fdbbc66ef6a7f7433bf603128cd60d5c804c70caac5f69c0", size = 6056449, upload-time = "2026-08-17T04:27:42.722Z" },
|
||||||
{ 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/75/0a/f1980039ab7bf8342c4aa4da2a4cafe40e6a840f456d5bbf86ba024fcdca/prek-0.4.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a79e5940354a789e1311172a960c5daa75e3f643a47ee2be85b7d2ca1d7d980a", size = 5839941, upload-time = "2026-08-17T04:27:44.306Z" },
|
||||||
{ 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/f1/e2/67f067dcb912e157bc7f4cd2aeb422a0de65e7744c675d7f439676f6c832/prek-0.4.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60b6539c1b28804807849173a6b49d467f6f36ca98bbb4536f5e34114d79942a", size = 5762723, upload-time = "2026-08-17T04:27:45.737Z" },
|
||||||
{ 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/84/39/6669fff5c3336a2b79cf853c86b95547cf9be720a2ee7e4ae5eaa6cb46a1/prek-0.4.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:24cfabd9e5b8c5546ecaef562841a41a255a9dec1b7ae7761ce5a7a024ab9dd9", size = 6090040, upload-time = "2026-08-17T04:27:47.185Z" },
|
||||||
{ 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/2a/23/c23210be4c89795a854e76146c2465aaffc3d85ea83df61c1d4d5bb41bca/prek-0.4.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1f08d368da45439f949885bb0b637bc7dfe6910140d5285fa8bae058697ddd06", size = 6570273, upload-time = "2026-08-17T04:27:48.673Z" },
|
||||||
{ 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/00/9c/4f10728ed36295347e84f01bb3e4c9078c9e337edc66215f5e2066b30551/prek-0.4.14-py3-none-win32.whl", hash = "sha256:5bfb30808ce2099c67d2a2d4cd68dc031e3fec1eeb61d73ef51a8f7c04d021cb", size = 5586715, upload-time = "2026-08-17T04:27:50.471Z" },
|
||||||
{ 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/4f/a7/d080a157d4e92927f7618d62d6a23979ef64a10c76a040cb8a8a194c50d7/prek-0.4.14-py3-none-win_amd64.whl", hash = "sha256:29364012d5704475d1092eb8a96ea30b163279096ad5e0c80a620fffa79bc639", size = 5969946, upload-time = "2026-08-17T04:27:51.971Z" },
|
||||||
{ 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" },
|
{ url = "https://files.pythonhosted.org/packages/37/e1/6fc64bb82e7270f61707e00b6d3154a4592ae0b2d3bd308173aa7aabe0a1/prek-0.4.14-py3-none-win_arm64.whl", hash = "sha256:ff588c02e10c8d05150763607671a22d5585c0ff7036c884d3489c2726eb215c", size = 5729906, upload-time = "2026-08-17T04:27:53.52Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometheus-client"
|
name = "prometheus-client"
|
||||||
version = "0.25.0"
|
version = "0.26.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.13.4"
|
version = "2.13.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-types" },
|
{ name = "annotated-types" },
|
||||||
@@ -855,111 +855,111 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-core"
|
name = "pydantic-core"
|
||||||
version = "2.46.4"
|
version = "2.46.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
|
{ url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
|
{ url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
|
{ url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
|
{ url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
|
{ url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
|
{ url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
|
{ url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
{ url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
{ url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
{ url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
{ url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
{ url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
{ url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
{ url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
{ url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
{ url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
{ url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
{ url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
{ url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
{ url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
{ url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -973,15 +973,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pymdown-extensions"
|
name = "pymdown-extensions"
|
||||||
version = "11.0"
|
version = "11.0.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
]
|
]
|
||||||
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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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" },
|
{ url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1307,27 +1307,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ty"
|
name = "ty"
|
||||||
version = "0.0.64"
|
version = "0.0.75"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/81/d0/d0c96f898d6974a4a3569ab3efdf9512c04ad99f9203effb55f72497fe97/ty-0.0.75.tar.gz", hash = "sha256:4c5eead33dfbf6e2ebb4f400f74b51ffc9bab702a6f23ddb648a1cbb740387e3", size = 6868326, upload-time = "2026-08-26T20:23:40.399Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ 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/cb/6c/b12d03505f17581f0cfa3c12273fe34c1d67b36dfda1bc561a6bdc16512b/ty-0.0.75-py3-none-linux_armv6l.whl", hash = "sha256:e5409f50db2246fd4bd039d93d261e0cfa1daa554a4fb77256f91072c570349a", size = 12972606, upload-time = "2026-08-26T20:22:59.716Z" },
|
||||||
{ 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/d1/aa/30f11eecd9215a9f87e8fe8baaf48f3ce905f5d75b8e4aac70f0091f130c/ty-0.0.75-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5e7b8b3472fb9bb2eeab314984b265df08a7a9d518867a9e6020eebc06570be2", size = 12527158, upload-time = "2026-08-26T20:23:02.767Z" },
|
||||||
{ 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/f2/11/7fd7001b0b5c6610bfbad7357e47d5fe6f82d4e84e94c53776a478f5e9f8/ty-0.0.75-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c6ccf34169821fe0d23e3360deeef981d217963412f1d087b9bdd32ec57f7a57", size = 12400533, upload-time = "2026-08-26T20:23:04.965Z" },
|
||||||
{ 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/fd/7f/1e284ea3d348d7be02f12d83bc22ed9ef193033f863f05b64db99027f141/ty-0.0.75-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842ebb41e9c6c334b40768704e20b1a69d5c6b08805b289d5e0e2565f49f2de1", size = 12420592, upload-time = "2026-08-26T20:23:07.427Z" },
|
||||||
{ 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/2d/ab/d813271543370c47fd74b5118f2066ab32b0983e907b1821f3f9a6d0fa7f/ty-0.0.75-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7a5a723c5f1e0fab4ffbfe9bd95123a526ed48f206e5f25cb2161ca294007a", size = 12739219, upload-time = "2026-08-26T20:23:09.809Z" },
|
||||||
{ 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/31/5b/95b49cc5570fd92a7bf63732f649b31906158721e03c7fcb1b5be74ee3bf/ty-0.0.75-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54382f98e5da292fcd7104391afef5105c35bb2f312e29bea6f5fa419935255c", size = 13494046, upload-time = "2026-08-26T20:23:12.191Z" },
|
||||||
{ 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/2c/0d/502d2dd68173cf020e1ad2bdbab9544c86776de0b0e2ed15f8c2fe006e3d/ty-0.0.75-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac13b180dc2aade2cd243f56b01650e78bf091a2e522ad3bc947245d7837c613", size = 13938899, upload-time = "2026-08-26T20:23:14.764Z" },
|
||||||
{ 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/20/5b/f3b12a25c07224456219fc2bd20db0ad7e40b304be0ff6aad728da0135f9/ty-0.0.75-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752df7951a443219d7f1ff817e3723c85d428565ff449e08a7a93ba821661526", size = 13656711, upload-time = "2026-08-26T20:23:17.145Z" },
|
||||||
{ 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/51/7b/f090ad306e2b15a07b332d647138c5264b89d9758855ecce8b8a10bcb153/ty-0.0.75-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd399feedf7cee816563c1baec45fc1c0b3c89f1ea42364920b688004b5b7da", size = 13093499, upload-time = "2026-08-26T20:23:19.489Z" },
|
||||||
{ 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/f1/4b/f69b99aaaca0c7c65d5f114b186b26b21666f767b0c69eec99a2bdccc061/ty-0.0.75-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d7625f6f56c7dc1e873579fdc9e432a0e21e302afe847ab60704d2303442a92e", size = 13520580, upload-time = "2026-08-26T20:23:21.789Z" },
|
||||||
{ 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/b6/e7/692c5f905c0345a15d2255fc74066d660f030254ae8dcdaf33f5a5c2f279/ty-0.0.75-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:89e7d527e95a2534b70cae29e94c104b84082760ea05927d23bb87280969c104", size = 12524095, upload-time = "2026-08-26T20:23:24.026Z" },
|
||||||
{ 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/ba/9a/f42b12cf265ea95344bf554764c4791cfb273bdd628aadd7c209af7cadc3/ty-0.0.75-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0843f134440740706e01bee5f88f4cfc10e9b018bddb9e4ef4c12dc9fc0c9aef", size = 12756591, upload-time = "2026-08-26T20:23:26.126Z" },
|
||||||
{ 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/7c/5e/9b180c133cb9cce48179a7d2bf9e1802d992aa8176a918e0e05205760b42/ty-0.0.75-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1bd0ec0e50ee1376875c88891efe6f549c3560fa5b2ddad79a425cd5a6218b9c", size = 12998754, upload-time = "2026-08-26T20:23:28.353Z" },
|
||||||
{ 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/39/f6/3c6ef5dd550103e29905121c67fb96a374564f31a2f44c6faa1af98c2d61/ty-0.0.75-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f9eafd561f90110d5e29f589ec3e956c4686e2f6631348d99276436f5cbe4d1", size = 13316474, upload-time = "2026-08-26T20:23:30.857Z" },
|
||||||
{ 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/bb/52/12776337874c821076bd5368e352ccd9e67174790abe3b856f749cb3524b/ty-0.0.75-py3-none-win32.whl", hash = "sha256:05063a6fafe2154b794a7f964515d148e51acd186d72d4a3acd347ee9fa19336", size = 12316315, upload-time = "2026-08-26T20:23:33.528Z" },
|
||||||
{ 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/53/e6/bb51e16af5c7138c9f52f8f3d0a401a371c6798d092e3b74926f186a9814/ty-0.0.75-py3-none-win_amd64.whl", hash = "sha256:81cf1ba5f6b7536ad56747865214255d9bc8e80533a689dbb9ddeaad464b09f1", size = 12917267, upload-time = "2026-08-26T20:23:35.978Z" },
|
||||||
{ 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" },
|
{ url = "https://files.pythonhosted.org/packages/39/73/4542f829107468b5de4231af67f29927c093bfad11f3c1e5b2c08fb1206b/ty-0.0.75-py3-none-win_arm64.whl", hash = "sha256:541c9af5b7a0ad23d15ec315a7da81150833c359f48124ed3789ff25eacd6f42", size = 12711024, upload-time = "2026-08-26T20:23:38.159Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user