Compare commits

...
5 Commits
Author SHA1 Message Date
dependabot[bot] d8d71a0566 ⬆ Bump ty from 0.0.64 to 0.0.74
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.64 to 0.0.74.
- [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.74)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 17:04:04 +00:00
d3vyce 6dafd40277 Merge pull request #381 from d3vyce/380-dependsdb-resolved-twice-in-one-request-opens-two-sessions-and-silently-discards-writes
fix: Depends(db) resolved twice in one request opens two sessions and silently discards writes
2026-08-28 19:01:43 +02:00
d3vyce 1c806cccd9 fix: Depends(db) resolved twice in one request opens two sessions and silently discards writes 2026-08-28 12:58:47 -04:00
d3vyce 1354f59bb4 fix: searching an Enum column raises UndefinedFunctionError 2026-08-28 18:48:06 +02:00
dependabot[bot] 23dc5c86b2 ⬆ Bump pymdown-extensions from 11.0 to 11.0.1
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 11.0 to 11.0.1.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/11.0...11.0.1)

---
updated-dependencies:
- dependency-name: pymdown-extensions
  dependency-version: 11.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 17:19:37 +02:00
7 changed files with 235 additions and 41 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ db = Database(
## 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:
+1 -1
View File
@@ -101,7 +101,7 @@ exclude = ["*.md"]
extend-select = ["E712"]
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends"]
extend-immutable-calls = ["fastapi.Depends", "fastapi.Security"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["RUF012", "RUF059", "SIM117", "DTZ001", "S110", "BLE001"]
+4 -2
View File
@@ -160,9 +160,11 @@ def build_search_filters(
column = field
# 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 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:
filters.append(column_as_string.like(f"%{query}%"))
+9 -6
View File
@@ -66,10 +66,8 @@ class _CommitOnResponseMiddleware:
async def send_wrapper(message: Message) -> None:
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")
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():
await session.commit()
await send(message)
@@ -158,7 +156,6 @@ class Database:
# Private, per-instance state attribute; cannot collide with another
# Database or be mismatched against the middleware.
self._state_attr = f"_ft_db_session_{id(self):x}"
self._middleware_installed = False
self._disposed = False
async def _dispose(self) -> None:
@@ -206,7 +203,6 @@ class Database:
```
"""
app.add_middleware(_CommitOnResponseMiddleware, state_attr=self._state_attr)
self._middleware_installed = True
inner_lifespan = app.router.lifespan_context
@@ -243,10 +239,17 @@ class Database:
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:
setattr(request.state, self._state_attr, 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()
@asynccontextmanager
+70
View File
@@ -388,6 +388,76 @@ class TestBuildSearchFilters:
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 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:
"""Tests for SearchConfig options."""
+128 -9
View File
@@ -6,7 +6,7 @@ from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import Depends, FastAPI
from fastapi import Depends, FastAPI, Security
from fastapi.responses import StreamingResponse
from httpx import ASGITransport, AsyncClient
from pydantic import PostgresDsn
@@ -287,23 +287,37 @@ class TestDatabaseDependency:
break
@pytest.mark.anyio
async def test_skips_commit_when_middleware_installed(self, engine, session_maker):
"""With ``install()``, the dependency must NOT commit — the middleware owns it.
async def test_second_resolution_borrows_session(self, engine):
"""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
open transaction is rolled back on session close and nothing persists.
"""
owner_gen = db(request)
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.install(FastAPI())
async for session in db(_make_request()):
role = Role(name="mw_owns_commit")
role = Role(name="mw_never_ran")
session.add(role)
await session.flush()
async with session_maker() as verify:
result = await RoleCrud.first(verify, [Role.name == "mw_owns_commit"])
assert result is None
result = await RoleCrud.first(verify, [Role.name == "mw_never_ran"])
assert result is not None
class TestDatabaseSession:
@@ -1523,6 +1537,55 @@ def _build_app(db: Database) -> FastAPI:
await session.commit()
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}")
async def stream_role(
name: str, session: AsyncSession = Depends(db)
@@ -1619,6 +1682,62 @@ class TestCommitIntegration:
# The write made before the stream began is durably committed.
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
async def test_multi_write_atomicity(self, engine, session_maker):
"""When the 2nd write fails, the 1st must roll back too (one txn)."""
Generated
+22 -22
View File
@@ -973,15 +973,15 @@ wheels = [
[[package]]
name = "pymdown-extensions"
version = "11.0"
version = "11.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ 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 = [
{ 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]]
@@ -1307,27 +1307,27 @@ wheels = [
[[package]]
name = "ty"
version = "0.0.64"
version = "0.0.74"
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/88/0f/c767853e88567a2ec7e996dd95e3105b1bc62c95d103689311ef0f4a603c/ty-0.0.74.tar.gz", hash = "sha256:da14344fc8625fc9ff359bafb856ad575636ea86d9bb6a629b146bff27b380e6", size = 6786318, upload-time = "2026-08-22T15:05:54.054Z" }
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/2c/95/6ded58bc97885c6d88fa1f9cd815031489200738f961cbf0466663213f80/ty-0.0.74-py3-none-linux_armv6l.whl", hash = "sha256:8969ef4e508debf00cf58f9ea85a539f799b1732c59cdfcecd037630b9755b30", size = 12790043, upload-time = "2026-08-22T15:05:05.015Z" },
{ url = "https://files.pythonhosted.org/packages/d9/8a/5e323603b6ab8731144421877ee8a0f8ac5a5511e67857127caa09f6730e/ty-0.0.74-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:51fb6cf5b98e1e1140825b2430943f78d744876a735231656eafbb4c3f7eca3c", size = 12371748, upload-time = "2026-08-22T15:05:08.609Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/ee72e08cb705281e8d8c42917dd577aa598a8a098008495fda5176ee3f6e/ty-0.0.74-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ebe60b1f0a948c793d6c77fc9e9ddda599e4f023c04ab16e8e03bcb428c3fa0", size = 12282403, upload-time = "2026-08-22T15:05:11.448Z" },
{ url = "https://files.pythonhosted.org/packages/da/b3/fd935b694ff68bc278af50f7ad04770b36ce6306399baef7e1847b553a9d/ty-0.0.74-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa97f407a695c890a53615966a663c7d2167e2cabe88db7ca1a24d62635cdfc8", size = 12345164, upload-time = "2026-08-22T15:05:14.19Z" },
{ url = "https://files.pythonhosted.org/packages/54/5c/5b5825268e029ebb164c909780103dbbae367f069801410068bf1cef29b3/ty-0.0.74-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:673ddb733d4a0db31385ba1ed9ff1f6bd9dc5565413ce57b1ca5ac4c7803da5d", size = 12556646, upload-time = "2026-08-22T15:05:16.994Z" },
{ url = "https://files.pythonhosted.org/packages/56/e7/515914e571d62ce0101744fed3f881936eeb1b30dc37beb72b4f7ca1e289/ty-0.0.74-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1028e7c6b4f6145e9704552f43a5fffdcd51b42263ffdcd9c9677762bc395a4a", size = 13311653, upload-time = "2026-08-22T15:05:20.254Z" },
{ url = "https://files.pythonhosted.org/packages/b0/07/d1452babb6f9266c2122cabc095180b70ed306fb770b2996753814d2237d/ty-0.0.74-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a8890493021fb308772474983316eb91f7b56cb227a6a05a06b262a36f0", size = 13768284, upload-time = "2026-08-22T15:05:23.197Z" },
{ url = "https://files.pythonhosted.org/packages/b1/60/8d4a2fc7842a47210a1cb0a16a187d9de39ad5d509a00fb74c1c073afcde/ty-0.0.74-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94859d321f3c6a6c8f7bfc3f40e8319cda7e6e012e613440f3dfd145d5010e2e", size = 13422306, upload-time = "2026-08-22T15:05:26.248Z" },
{ url = "https://files.pythonhosted.org/packages/de/76/ebbc269a8c4efcc4d44624993bd188145f20d60ebda9680b15aaec42cc50/ty-0.0.74-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:970a8b2c09ff3be04c8a1c6767332d861be4fce85efe7bb205e4ade7c8655274", size = 12970637, upload-time = "2026-08-22T15:05:29.15Z" },
{ url = "https://files.pythonhosted.org/packages/9e/dd/b99f7236acbf856780ca1779a48143d2d9f2c24d7f531a0ce15a022b8a87/ty-0.0.74-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:795f763b3ded85574c2c2846a6fb8acf2aa76e9e83d761143e92b1f0c7ffa2cd", size = 13344891, upload-time = "2026-08-22T15:05:32.033Z" },
{ url = "https://files.pythonhosted.org/packages/0b/d7/9ff7449a4c7e6428f2c6f298e74cf24b70668f29d45c249507a723ff3782/ty-0.0.74-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dc086db5367d912c31c0cc872deb7387290e779a4b9b54fcb944673a7cd52c7b", size = 12395272, upload-time = "2026-08-22T15:05:34.702Z" },
{ url = "https://files.pythonhosted.org/packages/b1/dd/b23a5b6b35d37df89dc8dc5daa09efd9245a668b50c4c81c25de21567dc1/ty-0.0.74-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0314d7b391cf684e47c2fa093d2ce4c597cfc9b01d9a315fe204aed6359b271b", size = 12573079, upload-time = "2026-08-22T15:05:37.683Z" },
{ url = "https://files.pythonhosted.org/packages/23/c5/ccba16239d6129533c8b3603458d0f4dd2ba69478e47059073968e74261d/ty-0.0.74-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4a45dd2e991e8bdae82ba78c8cd051b253f60bc71a6536598fa3ef580b4fc9b", size = 12832506, upload-time = "2026-08-22T15:05:40.505Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1c/2390912634dff4f341f97b397f2aee341ff062be0a66cda37d59375454f2/ty-0.0.74-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:210e2eac6b018fb934e2b8dac3956a0ba076a3fb1fa6f135058c825e5b759b81", size = 13154752, upload-time = "2026-08-22T15:05:43.355Z" },
{ url = "https://files.pythonhosted.org/packages/c4/33/a8c12188227e6f74f91853a7374e01ed81d6ad21c16c8b70e92dbebfe46a/ty-0.0.74-py3-none-win32.whl", hash = "sha256:db0bb6a8f098ef9bd1be861f73b4f7c0320d40d4c05c7ae0a8677d4e7aa4f6e5", size = 12130002, upload-time = "2026-08-22T15:05:46.058Z" },
{ url = "https://files.pythonhosted.org/packages/21/5c/064f28ccb9c234cfce5a2f7aa69a256663d5ae5bb0290b3a9706cc4d1e4c/ty-0.0.74-py3-none-win_amd64.whl", hash = "sha256:bebff181515255b3c78bd2e7693ae66fab6064ad4feea2065c68bc01022aa678", size = 12771435, upload-time = "2026-08-22T15:05:48.811Z" },
{ url = "https://files.pythonhosted.org/packages/fe/06/d6becdaca0315346c26b6df97cb0eafa81de4f870945d6989e88704374ed/ty-0.0.74-py3-none-win_arm64.whl", hash = "sha256:1a3469eaaf8c85b1c0a15bede25d36daea4b09fce1d913e965b24e24b3f1d6c6", size = 12558299, upload-time = "2026-08-22T15:05:51.543Z" },
]
[[package]]