Compare commits

...
18 Commits
Author SHA1 Message Date
d3vyce e09b911277 Merge pull request #393 from d3vyce/392-searchfalsefilterfalseorderfalse-dont-clear-the-response-metadata
fix: search=False/filter=False/order=False don't clear the response metadata
2026-09-01 19:46:46 +02:00
d3vyce f7ecb76e8d fix: search=False/filter=False/order=False don't clear the response metadata 2026-09-01 13:43:33 -04:00
d3vyce ef269833b9 Version 5.1.2 2026-08-31 16:51:38 -04:00
d3vyce 610b3e1ab4 revert: drop populate_existing reload in create()/update() 2026-08-31 16:51:04 -04:00
d3vyce 312723d66b Version 5.1.1 2026-08-29 17:48:03 -04:00
d3vyce 3d426ac567 Merge pull request #391 from d3vyce/dependabot/uv/ty-0.0.75
⬆ Bump ty from 0.0.64 to 0.0.75
2026-08-29 23:44:03 +02:00
d3vyce 0cc189117d fix: ty warning 2026-08-29 17:41:53 -04:00
dependabot[bot] a7b78832fd ⬆ Bump ty from 0.0.64 to 0.0.75
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.64 to 0.0.75.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ty/compare/0.0.64...0.0.75)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.75
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-29 21:40:22 +00:00
d3vyce 56c19971e2 Merge pull request #390 from d3vyce/dependabot/uv/prek-0.4.14
⬆ Bump prek from 0.4.9 to 0.4.14
2026-08-29 23:39:56 +02:00
d3vyce 7cd0ca2936 Merge pull request #378 from d3vyce/dependabot/uv/fastapi-0.141.1
⬆ Bump fastapi from 0.139.0 to 0.141.1
2026-08-29 23:38:14 +02:00
d3vyce 713e9a40c8 Merge pull request #375 from d3vyce/dependabot/uv/prometheus-client-0.26.0
⬆ Bump prometheus-client from 0.25.0 to 0.26.0
2026-08-29 23:38:04 +02:00
d3vyce 1432a2cc3a Merge pull request #374 from d3vyce/dependabot/uv/uv-build-gte-0.10-and-lt-0.13.0
⬆ Update uv-build requirement from <0.12.0,>=0.10 to >=0.10,<0.13.0
2026-08-29 23:37:55 +02:00
dependabot[bot] 375d349fc4 ⬆ Bump prek from 0.4.9 to 0.4.14
Bumps [prek](https://github.com/j178/prek) from 0.4.9 to 0.4.14.
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.4.9...v0.4.14)

---
updated-dependencies:
- dependency-name: prek
  dependency-version: 0.4.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-29 21:36:25 +00:00
d3vyce 7506fa3093 Merge pull request #389 from d3vyce/388-pathdependency-bodydependency-build-a-bare-unconfigured-crud
fix: PathDependency and BodyDependency build a bare unconfigured CRUD
2026-08-29 23:34:42 +02:00
d3vyce 943115562b fix: PathDependency and BodyDependency build a bare unconfigured CRUD 2026-08-29 08:39:23 -04:00
dependabot[bot] f1e50a947a ⬆ Bump fastapi from 0.139.0 to 0.141.1
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.139.0 to 0.141.1.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.139.0...0.141.1)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.141.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 17:04:01 +00:00
dependabot[bot] 7c73488f7d ⬆ Bump prometheus-client from 0.25.0 to 0.26.0
Bumps [prometheus-client](https://github.com/prometheus/client_python) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/prometheus/client_python/releases)
- [Commits](https://github.com/prometheus/client_python/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: prometheus-client
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 21:43:47 +00:00
dependabot[bot] 518e22e921 ⬆ Update uv-build requirement from <0.12.0,>=0.10 to >=0.10,<0.13.0
Updates the requirements on [uv-build](https://github.com/astral-sh/uv) to permit the latest version.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.10.0...0.12.0)

---
updated-dependencies:
- dependency-name: uv-build
  dependency-version: 0.12.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 21:43:35 +00:00
10 changed files with 366 additions and 147 deletions
+10
View File
@@ -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])
```
`search_fields`, `facet_fields` and `order_fields` follow the same override rule
everywhere they are accepted &mdash; `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 &mdash; 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):
```python
+30
View File
@@ -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)
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "fastapi-toolsets"
version = "5.1.0"
version = "5.1.2"
description = "Production-ready utilities for FastAPI applications"
readme = "README.md"
license = "MIT"
@@ -91,7 +91,7 @@ docs-src = [
]
[build-system]
requires = ["uv_build>=0.10,<0.12.0"]
requires = ["uv_build>=0.10,<0.13.0"]
build-backend = "uv_build"
[tool.ruff.format]
+1 -1
View File
@@ -24,4 +24,4 @@ Example usage:
return Response(data={"user": user.username}, message="Success")
"""
__version__ = "5.1.0"
__version__ = "5.1.2"
+45 -42
View File
@@ -270,30 +270,17 @@ class AsyncCrud(Generic[ModelType]):
return cls.default_load_options
@classmethod
def _capture_pk_values(cls: type[Self], instance: ModelType) -> dict[str, Any]:
"""Capture PK values off instance — call before commit expires attributes."""
return {
cast(str, col.key): getattr(instance, cast(str, col.key))
for col in cls.model.__mapper__.primary_key
}
@classmethod
async def _reload_with_options_by_pk(
cls: type[Self], session: AsyncSession, pk_values: dict[str, Any]
async def _reload_with_options(
cls: type[Self], session: AsyncSession, instance: DeclarativeBase
) -> ModelType:
"""Re-query by previously captured PK values, with default_load_options applied."""
# Only called when cls.default_load_options is set (see call sites).
"""Re-query instance by PK with default_load_options applied."""
mapper = cls.model.__mapper__
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))
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)
return await cls.get(session, filters=pk_filters)
@classmethod
async def _resolve_m2m(
@@ -399,13 +386,29 @@ class AsyncCrud(Generic[ModelType]):
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
def _resolve_search_columns(
cls: type[Self],
search_fields: Sequence[SearchFieldType] | None,
) -> list[str] | None:
"""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:
return None
return search_field_keys(fields)
@@ -416,7 +419,7 @@ class AsyncCrud(Generic[ModelType]):
order_fields: Sequence[OrderFieldType] | None,
) -> list[str] | None:
"""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:
return None
return sorted(facet_keys(fields))
@@ -495,9 +498,7 @@ class AsyncCrud(Generic[ModelType]):
order_field_map: dict[str, OrderFieldType] | None = None
order_valid_keys: list[str] | None = None
if order:
resolved_order = (
order_fields if order_fields is not None else cls.order_fields
)
resolved_order = cls._resolve_order_fields(order_fields)
if resolved_order:
keys = facet_keys(resolved_order)
order_field_map = dict(zip(keys, resolved_order))
@@ -523,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]:
result: dict[str, Any] = dict(pagination_fixed)
result: dict[str, Any] = dict(fixed)
for name in pagination_param_names:
result[name] = kwargs[name]
@@ -847,14 +861,9 @@ class AsyncCrud(Generic[ModelType]):
setattr(db_model, rel_attr, related_instances)
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)
if cls.default_load_options:
db_model = await cls._reload_with_options(session, db_model)
result = cast(ModelType, db_model)
if schema:
return Response(data=schema.model_validate(result))
@@ -1231,15 +1240,9 @@ class AsyncCrud(Generic[ModelType]):
m2m_resolved = await cls._resolve_m2m(session, obj, only_set=True)
for rel_attr, related_instances in m2m_resolved.items():
setattr(db_model, rel_attr, related_instances)
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)
if cls.default_load_options:
db_model = await cls._reload_with_options(session, db_model)
if schema:
return Response(data=schema.model_validate(db_model))
return db_model
+74 -55
View File
@@ -2,14 +2,15 @@
import inspect
import typing
from collections.abc import Callable
from collections.abc import Callable, Sequence
from typing import Any, cast
from fastapi import Depends
from fastapi.params import Depends as DependsClass
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
__all__ = ["BodyDependency", "PathDependency"]
@@ -24,12 +25,59 @@ def _unwrap_session_dep(session_dep: SessionDependency) -> Callable[..., Any]:
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(
model: type[ModelType],
field: Any,
*,
session_dep: SessionDependency,
param_name: str | None = None,
crud: type[AsyncCrud[ModelType]] | None = None,
load_options: Sequence[ExecutableOption] | None = None,
) -> ModelType:
"""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)
session_dep: Session dependency function (e.g., get_db)
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:
A Depends() instance that resolves to the model instance
@@ -55,36 +107,14 @@ def PathDependency(
): ...
```
"""
session_callable = _unwrap_session_dep(session_dep)
crud = CrudFactory(model)
name = (
param_name
if param_name is not None
else f"{model.__name__.lower()}_{field.key}"
return _fetch_dependency(
model,
field,
session_dep=session_dep,
param_name=param_name or 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(
@@ -93,6 +123,8 @@ def BodyDependency(
*,
session_dep: SessionDependency,
body_field: str,
crud: type[AsyncCrud[ModelType]] | None = None,
load_options: Sequence[ExecutableOption] | None = None,
) -> ModelType:
"""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)
session_dep: Session dependency function (e.g., get_db)
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:
A Depends() instance that resolves to the model instance
@@ -120,28 +156,11 @@ def BodyDependency(
): ...
```
"""
session_callable = _unwrap_session_dep(session_dep)
crud = CrudFactory(model)
python_type = field.type.python_type
async def dependency(
session: AsyncSession = Depends(session_callable), **kwargs: Any
) -> ModelType:
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 _fetch_dependency(
model,
field,
session_dep=session_dep,
param_name=body_field,
crud=crud,
load_options=load_options,
)
return cast(ModelType, Depends(cast(Callable[..., ModelType], dependency)))
+24
View File
@@ -466,6 +466,30 @@ class TestDefaultLoadOptionsIntegration:
assert updated.role is not None
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
async def test_load_options_overrides_default_load_options(
self, db_session: AsyncSession
+55
View File
@@ -2543,6 +2543,16 @@ class TestOrderParamsViaConsolidated:
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:
"""Tests for AsyncCrud.offset_paginate_params()."""
@@ -2612,6 +2622,9 @@ class TestOffsetPaginateParamsSchema:
"items_per_page": 10,
"include_total": False,
"include_facets": True,
"search_fields": [],
"facet_fields": [],
"order_fields": [],
}
@pytest.mark.anyio
@@ -2655,6 +2668,42 @@ class TestOffsetPaginateParamsSchema:
assert "search" 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):
"""filter=True with no facet_fields silently skips filter params."""
dep = RoleCrud.offset_paginate_params(search=False, filter=True, order=False)
@@ -2727,6 +2776,9 @@ class TestCursorPaginateParamsSchema:
"cursor": None,
"items_per_page": 5,
"include_facets": True,
"search_fields": [],
"facet_fields": [],
"order_fields": [],
}
@pytest.mark.anyio
@@ -2837,6 +2889,9 @@ class TestPaginateParamsSchema:
"items_per_page": 10,
"include_total": True,
"include_facets": True,
"search_fields": [],
"facet_fields": [],
"order_fields": [],
}
@pytest.mark.anyio
+79 -1
View File
@@ -7,15 +7,18 @@ from typing import Annotated, Any, cast
import pytest
from fastapi.params import Depends
from sqlalchemy import inspect as sa_inspect
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from fastapi_toolsets.crud import CrudFactory
from fastapi_toolsets.dependencies import (
BodyDependency,
PathDependency,
_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]:
@@ -275,3 +278,78 @@ class TestBodyDependency:
assert result.id == role.id
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)
)
Generated
+44 -44
View File
@@ -299,7 +299,7 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.139.0"
version = "0.141.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
@@ -308,14 +308,14 @@ dependencies = [
{ name = "typing-extensions" },
{ 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 = [
{ 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]]
name = "fastapi-toolsets"
version = "5.1.0"
version = "5.1.2"
source = { editable = "." }
dependencies = [
{ name = "asyncpg" },
@@ -814,35 +814,35 @@ wheels = [
[[package]]
name = "prek"
version = "0.4.9"
version = "0.4.14"
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 = [
{ url = "https://files.pythonhosted.org/packages/10/02/632446b72103daf92526203bf83cb76043ebce70588632aa2aea3741da07/prek-0.4.9-py3-none-linux_armv6l.whl", hash = "sha256:7b240ad6f679104309a944c4dd427ccc46d9aaf4f53ee07379c02bf7578c2750", size = 5637604, upload-time = "2026-07-11T11:03:38.985Z" },
{ url = "https://files.pythonhosted.org/packages/57/bf/89daefc85a1db9b8c525731c77121de0a8f57731e81c99d823e919e1f6a5/prek-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5cbb220d6d77cb047747dcabf421375fdfdd958e01a998a854758698d38fb239", size = 5960396, upload-time = "2026-07-11T11:03:40.953Z" },
{ url = "https://files.pythonhosted.org/packages/42/39/0e448da00671e77740be32cca96530ef64bcdbed178acce7f155b87f6057/prek-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd85df4c186becdc47b2e6155de8cace99133c7517387e30f08ca15b93ead11f", size = 5524982, upload-time = "2026-07-11T11:03:42.605Z" },
{ url = "https://files.pythonhosted.org/packages/71/e5/1beceff9cfcf02817c06f38e726c9875cd003f62cbfa516f282fb3c3109b/prek-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ba40511145e948d461d6641b556b6ff671b186b0c90743600b9dcb788dd5eb5c", size = 5793691, upload-time = "2026-07-11T11:03:44.106Z" },
{ url = "https://files.pythonhosted.org/packages/7e/17/99e4884c45c46be90e81e220d2bdd5020716963707ef6702361cc398dd91/prek-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0cc5c06ae448568d076bb5e7ce4d630879d6467b2a89fd79061c349a47826efa", size = 5544549, upload-time = "2026-07-11T11:03:45.564Z" },
{ url = "https://files.pythonhosted.org/packages/0c/4b/63f5fe22d867a6e07b12e8a7887a0f3b193c2ef0fa9ed80649f2d257f4ac/prek-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31af616c216ec7e47913802b082ca816d707d24738fa58eae0463ae296cbe71e", size = 5948798, upload-time = "2026-07-11T11:03:47.424Z" },
{ url = "https://files.pythonhosted.org/packages/1f/89/90d5005436afb6ab99d3ba6599820fe0f0fcb776f5e9e362b5a19e10cbea/prek-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1ef842d7f19879fac2135c42ba15508f2677346ba800bc8bb82e5f43fe6a6ec", size = 6696938, upload-time = "2026-07-11T11:03:49Z" },
{ url = "https://files.pythonhosted.org/packages/2a/62/068dd25e1106e262b0ce69ffcdc594cf52d85aa13f64628f851e458980d4/prek-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442ecf6a454c692bc8a2d5a16936a0fe8e3419727ff208e15818932acca9923a", size = 6170870, upload-time = "2026-07-11T11:03:50.407Z" },
{ url = "https://files.pythonhosted.org/packages/8e/42/bb1f3f3d84af4d12bb675ff071305fa776d3525c10626465d9960ad93c21/prek-0.4.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5b3c590252e3d5724ebed3774695712ff7cb554743b25184808aa5f42b06bd4c", size = 5800355, upload-time = "2026-07-11T11:03:51.882Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ca/dfc8312b4a7ce8fa100ce37a843279d108eec7e7fe2ddbe069448acd1642/prek-0.4.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dd283b15dec4da29caa3910bd72c8c9d7df93209770503465ad109d6370cb8e", size = 5655126, upload-time = "2026-07-11T11:03:53.388Z" },
{ url = "https://files.pythonhosted.org/packages/25/8a/b19413cb64b81c18502ea7bbef32897f9126b8e53b58cd656996573dc53c/prek-0.4.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:cc25e30e1700a5c7dd9bad665c321e5589b51502bb1bbc6ada45e326d08b428b", size = 5517839, upload-time = "2026-07-11T11:03:54.884Z" },
{ url = "https://files.pythonhosted.org/packages/94/af/900df3f7535e87045df331646c6a01bef6e77dba7f2bdb53483f30cbb988/prek-0.4.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4ff5947deeb9a92e6508dfba8b27962dfb927bf60fd36472c9ae862df96fb38c", size = 5802556, upload-time = "2026-07-11T11:03:56.787Z" },
{ url = "https://files.pythonhosted.org/packages/e9/7b/80e560cbe396d0f8687012769cb2d7d7f3428dd019549225ebf205dea806/prek-0.4.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8320ca167d41855d9c4fed66df599f31f96307cbb0da1311a9fe465152e20bd5", size = 6285747, upload-time = "2026-07-11T11:03:58.099Z" },
{ url = "https://files.pythonhosted.org/packages/a3/d4/01ae3b99d09559a69befd128859d0036c604608dd9c6c99986592dde3c21/prek-0.4.9-py3-none-win32.whl", hash = "sha256:b1e8d3bc88ddce6414853468ed8126f45d4ae20f2f4677801ade20ad67a826fa", size = 5320862, upload-time = "2026-07-11T11:03:59.803Z" },
{ url = "https://files.pythonhosted.org/packages/28/68/d038b14f0220fed197be8bc93229e6ea7ad460803ff8a26e4b14a8c81f66/prek-0.4.9-py3-none-win_amd64.whl", hash = "sha256:ed1b4f87a13d1565e8731c60db7fa058966049cbb4d8872d160add510a286558", size = 5706850, upload-time = "2026-07-11T11:04:01.207Z" },
{ url = "https://files.pythonhosted.org/packages/02/4a/57d04de49f591088901794cc22f36563a102681e3512ae17ee6085cd2f30/prek-0.4.9-py3-none-win_arm64.whl", hash = "sha256:7eab3900d9ea614c8ea0d0d55a8b708f0c88e43c966dc8b13a4e36c1e398dd16", size = 5540477, upload-time = "2026-07-11T11:04:02.815Z" },
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
name = "prometheus-client"
version = "0.25.0"
version = "0.26.0"
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 = [
{ 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]]
@@ -1307,27 +1307,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.64"
version = "0.0.75"
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 = [
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]