From cd036604bd1ca52cfe3132c0bf0485907fe5c10c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 15:05:58 +0000 Subject: [PATCH] Upgrade ty to 0.0.69 and fix typing bugs it newly catches Bumps the pinned `ty` type checker from 0.0.65 to 0.0.69. The newer version's more accurate typeshed bundling and stricter checking surfaced several latent bugs that were previously masked: - `SimpleCookie[str]` was an invalid subscript (SimpleCookie isn't generic in typeshed); drop it. - `ImmutableList` was declared as a plain `tuple` subclass despite being used as `ImmutableList[T]` everywhere; make it properly generic (`class ImmutableList[T](tuple[T, ...])`). - Fixing ImmutableList's genericity made plain-postgres's Meta properties correctly typed for the first time, which surfaced ~37 further errors from code that duck-types across heterogeneous Field subtypes. Left those collections as bare `ImmutableList` (except `related_objects`, which is homogeneous and stays precisely typed) rather than forcing full parametrization that the field hierarchy doesn't support yet. - plain-admin's AdminViewset wired cross-linked URLs onto view classes via dynamic attribute assignment suppressed with `# ty: ignore[unresolved-attribute]`. Hoisted the five duplicated URL-getter stub methods onto the shared AdminView base class and typed `get_views()` against AdminView instead of the generic View, so the assignments type-check for real and the ignores are gone. - plain-auth's test `login_client` helper built a cookie-attributes dict that didn't type-check against Morsel's update() signature; rebuilt it on top of `Response.set_cookie()` (the same path `plain.sessions`'s middleware uses), which also fixes a staleness bug where the test helper omitted `httponly`/`samesite`. Net effect: type-ignore count across the workspace drops from 160 to 153, with zero new ignores added. `./scripts/type-validate` and the full test suite pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EzceyCD3hNuNjQwi1pLDej --- plain-admin/plain/admin/cards/base.py | 2 +- plain-admin/plain/admin/views/base.py | 17 +++++ plain-admin/plain/admin/views/objects.py | 75 ----------------------- plain-admin/plain/admin/views/viewsets.py | 22 ++++--- plain-auth/plain/auth/test.py | 26 +++++--- plain-postgres/plain/postgres/meta.py | 53 +++------------- plain-postgres/plain/postgres/query.py | 2 +- plain/plain/test/client.py | 6 +- plain/plain/utils/datastructures.py | 4 +- pyproject.toml | 2 +- uv.lock | 44 ++++++------- 11 files changed, 82 insertions(+), 171 deletions(-) diff --git a/plain-admin/plain/admin/cards/base.py b/plain-admin/plain/admin/cards/base.py index b5dfc00503..dab590a544 100644 --- a/plain-admin/plain/admin/cards/base.py +++ b/plain-admin/plain/admin/cards/base.py @@ -93,6 +93,6 @@ def get_current_filter(self) -> str: def get_filters(self) -> list[str] | Enum | None: if isinstance(self.filters, list): # Avoid mutating the class attribute - return self.filters.copy() # type: ignore + return self.filters.copy() else: return self.filters diff --git a/plain-admin/plain/admin/views/base.py b/plain-admin/plain/admin/views/base.py index 3e0f9052b3..49478786db 100644 --- a/plain-admin/plain/admin/views/base.py +++ b/plain-admin/plain/admin/views/base.py @@ -162,6 +162,23 @@ def get_view_url(cls, obj: Any = None) -> str: else: return reverse(f"{_URL_NAMESPACE}:" + cls.view_name()) + # Wired up by AdminViewset.get_views() to cross-link the List/Create/Detail/ + # Update/Delete views registered together on a viewset. + def get_list_url(self) -> str: + return "" + + def get_create_url(self) -> str: + return "" + + def get_detail_url(self, obj: Any) -> str: + return "" + + def get_update_url(self, obj: Any) -> str: + return "" + + def get_delete_url(self, obj: Any) -> str: + return "" + def get_links(self) -> dict[str, str]: return self.links.copy() diff --git a/plain-admin/plain/admin/views/objects.py b/plain-admin/plain/admin/views/objects.py index c3d86c1fdc..33e84b15fc 100644 --- a/plain-admin/plain/admin/views/objects.py +++ b/plain-admin/plain/admin/views/objects.py @@ -248,21 +248,6 @@ def get_filter_names(self) -> list[str]: def get_object_id(self, obj: Any) -> Any: return self.get_field_value(obj, "id") - def get_list_url(self) -> str: - return "" - - def get_create_url(self) -> str: - return "" - - def get_detail_url(self, obj: Any) -> str: - return "" - - def get_update_url(self, obj: Any) -> str: - return "" - - def get_delete_url(self, obj: Any) -> str: - return "" - def get_object_url(self, obj: Any) -> str: if url := self.get_detail_url(obj): return url @@ -296,21 +281,6 @@ class AdminCreateView(AdminView, CreateView): template_name = None nav_section = None - def get_list_url(self) -> str: - return "" - - def get_create_url(self) -> str: - return "" - - def get_detail_url(self, obj: Any) -> str: - return "" - - def get_update_url(self, obj: Any) -> str: - return "" - - def get_delete_url(self, obj: Any) -> str: - return "" - def get_success_url(self, form: "BaseForm") -> str: if list_url := self.get_list_url(): return list_url @@ -337,21 +307,6 @@ def get_template_names(self) -> list[str]: "admin/detail.html", # A generic detail view for rendering any object ] - def get_list_url(self) -> str: - return "" - - def get_create_url(self) -> str: - return "" - - def get_detail_url(self, obj: Any) -> str: - return "" - - def get_update_url(self, obj: Any) -> str: - return "" - - def get_delete_url(self, obj: Any) -> str: - return "" - def get_fields(self) -> list[str]: return self.fields.copy() # Avoid mutating the class attribute itself @@ -374,21 +329,6 @@ class AdminUpdateView(AdminView, UpdateView): template_name = None nav_section = None - def get_list_url(self) -> str: - return "" - - def get_create_url(self) -> str: - return "" - - def get_detail_url(self, obj: Any) -> str: - return "" - - def get_update_url(self, obj: Any) -> str: - return "" - - def get_delete_url(self, obj: Any) -> str: - return "" - def get_links(self) -> dict[str, str]: links = super().get_links() @@ -420,21 +360,6 @@ class AdminDeleteView(AdminView, DeleteView): template_name = "admin/delete.html" nav_section = None - def get_list_url(self) -> str: - return "" - - def get_create_url(self) -> str: - return "" - - def get_detail_url(self, obj: Any) -> str: - return "" - - def get_update_url(self, obj: Any) -> str: - return "" - - def get_delete_url(self, obj: Any) -> str: - return "" - def get_links(self) -> dict[str, str]: links = super().get_links() diff --git a/plain-admin/plain/admin/views/viewsets.py b/plain-admin/plain/admin/views/viewsets.py index 0d02adb8d9..481d03d893 100644 --- a/plain-admin/plain/admin/views/viewsets.py +++ b/plain-admin/plain/admin/views/viewsets.py @@ -1,9 +1,9 @@ -from plain.views import View +from .base import AdminView class AdminViewset: @classmethod - def get_views(cls) -> list[type[View]]: + def get_views(cls) -> list[type[AdminView]]: """Views are defined as inner classes on the viewset class.""" # Primary views that we can interlink automatically @@ -30,26 +30,28 @@ def get_views(cls) -> list[type[View]]: views = [] for attr in cls.__dict__.values(): - if isinstance(attr, type) and issubclass(attr, View): + if isinstance(attr, type) and issubclass(attr, AdminView): views.append(attr) for view in views: - # Dynamic attributes stamped onto the view class by the viewset. - view.viewset = cls # ty: ignore[unresolved-attribute] + view.viewset = cls + # Wire up the classmethod that generates each sibling view's own + # URL as this view's getter, so List/Create/Detail/Update/Delete + # views registered together on a viewset can cross-link. if ListView: - view.get_list_url = ListView.get_view_url # ty: ignore[unresolved-attribute] + view.get_list_url = ListView.get_view_url if CreateView: - view.get_create_url = CreateView.get_view_url # ty: ignore[unresolved-attribute] + view.get_create_url = CreateView.get_view_url if DetailView: - view.get_detail_url = DetailView.get_view_url # ty: ignore[unresolved-attribute] + view.get_detail_url = DetailView.get_view_url if UpdateView: - view.get_update_url = UpdateView.get_view_url # ty: ignore[unresolved-attribute] + view.get_update_url = UpdateView.get_view_url if DeleteView: - view.get_delete_url = DeleteView.get_view_url # ty: ignore[unresolved-attribute] + view.get_delete_url = DeleteView.get_view_url return views diff --git a/plain-auth/plain/auth/test.py b/plain-auth/plain/auth/test.py index 2b1d70611c..5a67b842f2 100644 --- a/plain-auth/plain/auth/test.py +++ b/plain-auth/plain/auth/test.py @@ -3,6 +3,7 @@ from http.cookies import SimpleCookie from typing import TYPE_CHECKING, Any +from plain.http import Response from plain.http.request import Request from plain.runtime import settings from plain.sessions import SessionStore @@ -26,16 +27,21 @@ def login_client(client: Client, user: Any) -> None: login(request, user) session = get_request_session(request) session.save() - session_cookie = settings.SESSION_COOKIE_NAME - client.cookies[session_cookie] = session.session_key - cookie_data = { - "max-age": None, - "path": "/", - "domain": settings.SESSION_COOKIE_DOMAIN, - "secure": settings.SESSION_COOKIE_SECURE or None, - "expires": None, - } - client.cookies[session_cookie].update(cookie_data) + assert session.session_key is not None, "Session key should exist after save()" + + # Build the same Set-Cookie a real response would send, matching + # plain.sessions' SessionMiddleware, then copy it onto the test client. + response = Response() + response.set_cookie( + settings.SESSION_COOKIE_NAME, + session.session_key, + domain=settings.SESSION_COOKIE_DOMAIN, + path=settings.SESSION_COOKIE_PATH, + secure=bool(settings.SESSION_COOKIE_SECURE), + httponly=bool(settings.SESSION_COOKIE_HTTPONLY), + samesite=settings.SESSION_COOKIE_SAMESITE, + ) + client.cookies.update(response.cookies) def logout_client(client: Client) -> None: diff --git a/plain-postgres/plain/postgres/meta.py b/plain-postgres/plain/postgres/meta.py index 0a30cc0148..4c6e4c0c32 100644 --- a/plain-postgres/plain/postgres/meta.py +++ b/plain-postgres/plain/postgres/meta.py @@ -5,7 +5,7 @@ from collections import defaultdict from collections.abc import Iterable from functools import cached_property -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any from plain.postgres.exceptions import FieldDoesNotExist from plain.postgres.query import QuerySet @@ -175,7 +175,7 @@ def add_field(self, field: Field) -> None: self._expire_cache(reverse=False) @cached_property - def fields(self) -> ImmutableList[Field]: + def fields(self) -> ImmutableList: from plain.postgres.fields.related import RelatedField """ @@ -213,7 +213,7 @@ def is_not_a_generic_relation(f: Any) -> bool: ) @cached_property - def concrete_fields(self) -> ImmutableList[Field]: + def concrete_fields(self) -> ImmutableList: """ Return a list of all concrete fields on the model and its parents. @@ -226,7 +226,7 @@ def concrete_fields(self) -> ImmutableList[Field]: ) @cached_property - def local_concrete_fields(self) -> ImmutableList[Field]: + def local_concrete_fields(self) -> ImmutableList: """ Return a list of all concrete fields on the model. @@ -239,7 +239,7 @@ def local_concrete_fields(self) -> ImmutableList[Field]: ) @cached_property - def many_to_many(self) -> ImmutableList[Field]: + def many_to_many(self) -> ImmutableList: """ Return a list of all many to many fields on the model and its parents. @@ -407,19 +407,7 @@ def _expire_cache(self, forward: bool = True, reverse: bool = True) -> None: delattr(self, cache_key) self._get_fields_cache = {} - @overload - def get_fields( - self, include_reverse: Literal[False] = False - ) -> ImmutableList[Field]: ... - - @overload - def get_fields( - self, include_reverse: Literal[True] - ) -> ImmutableList[Field | ForeignObjectRel]: ... - - def get_fields( - self, include_reverse: bool = False - ) -> ImmutableList[Field | ForeignObjectRel]: + def get_fields(self, include_reverse: bool = False) -> ImmutableList: """ Return a list of fields associated to the model. @@ -434,40 +422,13 @@ def get_fields( """ return self._get_fields(reverse=include_reverse) - @overload - def _get_fields( - self, - *, - forward: Literal[True] = True, - reverse: Literal[False], - seen_models: set[type[Any]] | None = None, - ) -> ImmutableList[Field]: ... - - @overload - def _get_fields( - self, - *, - forward: Literal[False], - reverse: Literal[True] = True, - seen_models: set[type[Any]] | None = None, - ) -> ImmutableList[ForeignObjectRel]: ... - - @overload - def _get_fields( - self, - *, - forward: bool = True, - reverse: bool = True, - seen_models: set[type[Any]] | None = None, - ) -> ImmutableList[Field | ForeignObjectRel]: ... - def _get_fields( self, *, forward: bool = True, reverse: bool = True, seen_models: set[type[Any]] | None = None, - ) -> ImmutableList[Field | ForeignObjectRel]: + ) -> ImmutableList: """ Internal helper function to return fields of the model. diff --git a/plain-postgres/plain/postgres/query.py b/plain-postgres/plain/postgres/query.py index d576435516..cb80697514 100644 --- a/plain-postgres/plain/postgres/query.py +++ b/plain-postgres/plain/postgres/query.py @@ -703,7 +703,7 @@ def bulk_create( update_fields_objs, unique_fields_objs, ) - fields = meta.concrete_fields + fields = list(meta.concrete_fields) self._prepare_for_bulk_create(objs) with transaction.atomic(savepoint=False): objs_with_id, objs_without_id = partition(lambda o: o.id is None, objs) diff --git a/plain/plain/test/client.py b/plain/plain/test/client.py index 8a1007a6ae..1fb6f404a7 100644 --- a/plain/plain/test/client.py +++ b/plain/plain/test/client.py @@ -289,7 +289,7 @@ def __init__( ) -> None: self.json_encoder = json_encoder self._default_headers: dict[str, str] = headers or {} - self.cookies: SimpleCookie[str] = SimpleCookie() + self.cookies: SimpleCookie = SimpleCookie() def _build_request( self, @@ -558,12 +558,12 @@ def __init__( self.raise_request_exception = raise_request_exception @property - def cookies(self) -> SimpleCookie[str]: + def cookies(self) -> SimpleCookie: """Access the cookies from the request factory.""" return self._request_factory.cookies @cookies.setter - def cookies(self, value: SimpleCookie[str]) -> None: + def cookies(self, value: SimpleCookie) -> None: """Set the cookies on the request factory.""" self._request_factory.cookies = value diff --git a/plain/plain/utils/datastructures.py b/plain/plain/utils/datastructures.py index 3dffe94a30..188bfc5fdb 100644 --- a/plain/plain/utils/datastructures.py +++ b/plain/plain/utils/datastructures.py @@ -234,7 +234,7 @@ def dict(self) -> builtins.dict[str, Any]: return {key: self[key] for key in self} -class ImmutableList(tuple): +class ImmutableList[T](tuple[T, ...]): """ A tuple-like object that raises useful errors when it is asked to mutate. @@ -254,7 +254,7 @@ def __new__( *args: Any, warning: str = "ImmutableList object is immutable.", **kwargs: Any, - ) -> ImmutableList: + ) -> ImmutableList[T]: self = tuple.__new__(cls, *args, **kwargs) self.warning = warning return self diff --git a/pyproject.toml b/pyproject.toml index 26beece828..7317e9f6d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dev = [ "plain-tunnel", "plain-vendor", # Type checking and better dev experience - "ty>=0.0.65", + "ty>=0.0.69", "psycopg[binary]>=3.2.12", ] diff --git a/uv.lock b/uv.lock index 2141d35b01..b165a523a9 100644 --- a/uv.lock +++ b/uv.lock @@ -1635,7 +1635,7 @@ dev = [ { name = "plain-tunnel", editable = "plain-tunnel" }, { name = "plain-vendor", editable = "plain-vendor" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.12" }, - { name = "ty", specifier = ">=0.0.65" }, + { name = "ty", specifier = ">=0.0.69" }, ] [[package]] @@ -2119,27 +2119,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.65" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cf561927e8e9ab5c1892a833b664aa9cd6f051a75f6280c66d8047246bda/ty-0.0.65.tar.gz", hash = "sha256:b7134bffcc00b715fa8291e84d845782ced810a998dc1f7f11d71c85c4046325", size = 6460098, upload-time = "2026-07-29T18:31:03.27Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/4e/71e2d325d2b53a1afad81624ad076b2ede413213fc4a18cb05b78c568571/ty-0.0.65-py3-none-linux_armv6l.whl", hash = "sha256:dc556c9f05408bef4c4ef02b2cc382e4e5f797b4b20d64410289848f0d76705f", size = 12298466, upload-time = "2026-07-29T18:30:12.744Z" }, - { url = "https://files.pythonhosted.org/packages/57/77/fec8f29647c55794efa430a7f365e44f5ce7ffb6459d9445a87fac569bec/ty-0.0.65-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d2e0d34cc0a28a17ef0cf81135c5ebabc3562131f9079138ba5e7bae0f56bd", size = 11942421, upload-time = "2026-07-29T18:30:16.076Z" }, - { url = "https://files.pythonhosted.org/packages/13/09/7f3766aef9dc627e2698cf4e3e59cf53389dcae3812040d33c1aa931230f/ty-0.0.65-py3-none-macosx_11_0_arm64.whl", hash = "sha256:685f49a9312bbf69d5b65bbb66384fed1f927403ea030c217b9289092d7e46c4", size = 11451922, upload-time = "2026-07-29T18:30:19.155Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/1a77cd50e0befb50f55b8bf9bd3ed3eddf184bf28c61b56727039e0774fc/ty-0.0.65-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f564b5ebe78e2f3a8e7b8eacb1292eb88b7c0f3c8630671cfca31abc0709cd9", size = 11994999, upload-time = "2026-07-29T18:30:22.315Z" }, - { url = "https://files.pythonhosted.org/packages/63/7b/feda16f3a4a0a99be27431e0c9598eeeec0db1eb2fec9a15976698209418/ty-0.0.65-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c983e156fe9e113fb56389e13d327b6b8549fe866de9b269684723a88e9b732d", size = 12090662, upload-time = "2026-07-29T18:30:24.93Z" }, - { url = "https://files.pythonhosted.org/packages/ed/3e/3f69bf9c9307dbdc0771719f65ce5b556e7bdeeaccbdd599d4f57866d801/ty-0.0.65-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3663b7396e8b1a9954e20e732de7ccb0192bf4118473069b4945920d6923921", size = 12822094, upload-time = "2026-07-29T18:30:28.012Z" }, - { url = "https://files.pythonhosted.org/packages/90/38/8fa791b3bb503ee2b46ad81690cd1bdd54519582df6d805cee57fe143e85/ty-0.0.65-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306ed01f29d6e108e98feb233dbbf5878a027603b71bd3743b343977933a9f16", size = 13357833, upload-time = "2026-07-29T18:30:31.122Z" }, - { url = "https://files.pythonhosted.org/packages/c1/73/4dda396a201e1dd0ed3594a9b48e559cb41c4bc048c6cd4c4d1b39eb4313/ty-0.0.65-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28bcfc8898c94f079a9100e684bcf312b6a64ad3a7d4ebb35a4591546030a2cd", size = 12977303, upload-time = "2026-07-29T18:30:33.944Z" }, - { url = "https://files.pythonhosted.org/packages/a5/26/c250c2c569adc53a8591716641388397bcb2a442e4a30b952ae81b50c0e0/ty-0.0.65-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a75bd0c245c38802a8f488378e74f92feb7dd33db7d63fbdd6fdf82791ba730", size = 12579338, upload-time = "2026-07-29T18:30:37.199Z" }, - { url = "https://files.pythonhosted.org/packages/d3/94/4a5647d44753ca218fc930d7e4d9bf468d0ed4a0ad4b3d57588bc1bbacf7/ty-0.0.65-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9e5e1bdea9662d2b5312b4e99f319f4e6e2ea427511b5fbc546141b79ec53f76", size = 12957731, upload-time = "2026-07-29T18:30:39.937Z" }, - { url = "https://files.pythonhosted.org/packages/36/b6/1e22fa11a1e0dfb20b1c7f3cbfd8170273aada2a82f9ecd3055275370c44/ty-0.0.65-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:03a88493d4842889f65280ae241e06b399d57eb3c63571054cad21a4c33b3b69", size = 11938625, upload-time = "2026-07-29T18:30:42.603Z" }, - { url = "https://files.pythonhosted.org/packages/5c/0a/fe5f22ef62b193201bc5566762e22049762cd485bfafb5095a7050760054/ty-0.0.65-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:600b8bf6f4940cf7ffb2f43d3716faaf38dcb97cd8617c55771451bc0276408f", size = 12105592, upload-time = "2026-07-29T18:30:45.419Z" }, - { url = "https://files.pythonhosted.org/packages/76/fd/922b3a6e9d697452cdbb4b7e3f636868add5ec652154518a736e4364f3b7/ty-0.0.65-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c28007bc79d648c1ddaf1e65885d07baec48eb87240da442f608e4107c1b7d8", size = 12387335, upload-time = "2026-07-29T18:30:48.405Z" }, - { url = "https://files.pythonhosted.org/packages/77/22/a1a08ebc84c083db2fb55e3b5cd186db0c067692f4921146f601360231e2/ty-0.0.65-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c852da96091ad22361e6586b7c7ba98e1334dcd4d8ffb67e47f4fb673de33f77", size = 12682710, upload-time = "2026-07-29T18:30:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/eaaa410a25bbdea19722109b5422380a0e211b3afcf3071d15953ddbd5db/ty-0.0.65-py3-none-win32.whl", hash = "sha256:cf529d538f1403b14b0511e6ec3cdb95d3d974adabf24cc76cedc533368c3edc", size = 11692341, upload-time = "2026-07-29T18:30:54.35Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0f/6d48f206dce9d7e53fe3b5ea0f0ab5800dd9d2365b2b48f736783436c43f/ty-0.0.65-py3-none-win_amd64.whl", hash = "sha256:234a321e33c7cbbfbd67bfa0b01b685dd9c21f1841781a21e5ca1fa0b25f1d5d", size = 12729355, upload-time = "2026-07-29T18:30:57.275Z" }, - { url = "https://files.pythonhosted.org/packages/96/aa/7446f7725e303cf78e058c893af1f0552b9451895454908706f4c6c3494b/ty-0.0.65-py3-none-win_arm64.whl", hash = "sha256:b9424be1ec56d93ff18609fb1c0a0a2283fe1282cd6d1c7604f97d73b94d61f2", size = 12051375, upload-time = "2026-07-29T18:31:00.579Z" }, +version = "0.0.69" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5b/7a618632dfe9373b7df572ecd7a08c8f799d772fbc317da82dd3aa363207/ty-0.0.69.tar.gz", hash = "sha256:b65106e9ff24fa76e25e1142fb09c85244e815c40450e3021d2bf652c231bb43", size = 6565094, upload-time = "2026-08-06T10:04:25.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/60/6534092f4d2c15e2491807edd609c2e50d527c1fed957acf40b9f110b64a/ty-0.0.69-py3-none-linux_armv6l.whl", hash = "sha256:98bfd383b273540829af673e7f98b9c1c4bcc8547d12a1a3806cd0bec7f0e087", size = 12364185, upload-time = "2026-08-06T10:03:47.137Z" }, + { url = "https://files.pythonhosted.org/packages/34/2b/5c29689bd4f74c2e3394d983d85e4011b629f2ce3730c9442553b8554bf8/ty-0.0.69-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:964621ddd05771660017c51b4e74078d861d9fc863c21ef2a500db1ab62c9ccf", size = 12042510, upload-time = "2026-08-06T10:03:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/09/46/fa085bde4d23516d7ef14b24736fc5dd7dc498f60f52b3d077e59ffdea20/ty-0.0.69-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3ffea4048dd0da4c9c97393b4be0901098a9065b06fa81be2477cbde65d8a151", size = 11549397, upload-time = "2026-08-06T10:03:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/97b9efb2061dcab6fef1e94a4ad99df0bb45bd2cc15d4f5794c787ee0552/ty-0.0.69-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8684d4a70aadd1eab0f41bdba835e3288ef49db8402a8e6ca81bab52ed5d610", size = 12115567, upload-time = "2026-08-06T10:03:53.79Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c1/a5e0404965093835f3e62544e661784ec0aa8ef0b006ed50af50b19c107e/ty-0.0.69-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afaaba240ab4122e2069a796836d10be81b4ddb053ae268b3dff962a0b4ca5c7", size = 12149770, upload-time = "2026-08-06T10:03:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8cad6b205a4abe8a044ca0c84aea71e8ccda29b07a75a5f090e310605580/ty-0.0.69-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ea63ef07d4e33aeb1a775cf5f2c736b3ed22fa6f8b1b608591612c36795044", size = 12941278, upload-time = "2026-08-06T10:03:58.324Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/8766d96b732c2a060d70dc8ccafcc4d6a54109a2a95f1deb0705de88892b/ty-0.0.69-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb3730b1268e92a2907d7aea3afe8dd1b360ae65862f0557080cf479d481b424", size = 13426509, upload-time = "2026-08-06T10:04:00.621Z" }, + { url = "https://files.pythonhosted.org/packages/02/1f/e991b2cde953ea5b94d6a9a4c45c87937bd916bc09235f764407bf471c0a/ty-0.0.69-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a544ff57a752ef186ed40b5a2f44c17402af4cdefeb74a311ca02ebd57c4fca0", size = 13106582, upload-time = "2026-08-06T10:04:02.818Z" }, + { url = "https://files.pythonhosted.org/packages/ea/bb/73538f1b99e3558fd9db87b98698426f0f60fc8666da0b1efd0e70e275eb/ty-0.0.69-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87ed2cbca20caddfdf8e3e14d213ce91b67e75feed78900f4aaf3ef884954028", size = 12708931, upload-time = "2026-08-06T10:04:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/484a5208d74c4ad1155933906295ccdce9aa81a257d8df2ab9e41bd60133/ty-0.0.69-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2684efcbce5b6fe45045faf610b377b50781b6d2aa7e61ea23ecf5b3d2bce421", size = 12985322, upload-time = "2026-08-06T10:04:07.587Z" }, + { url = "https://files.pythonhosted.org/packages/6e/81/b75003f0d4da9ab3bc8fd4f4802f836cb9921ff7e70f460604f7b769a0b5/ty-0.0.69-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da9aeb26fdac1d2214937542b59e0d4d1ba94ec7a3f45444f33c846de1eb1d63", size = 12063910, upload-time = "2026-08-06T10:04:09.835Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/088469f547ef63dceefc4a75826aedee5014f9371dc5171cde931896a82c/ty-0.0.69-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:00e7677cd14ede381f705f71104ea7b8ea0ce217a8634e19a89781953de0e9ad", size = 12166823, upload-time = "2026-08-06T10:04:12.114Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c9/ce88a0bec0d46d8ae180b99c6ec014866fecc4cba1727b5feec8877b2765/ty-0.0.69-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d91965eb799649833d0d6042db09cd03d15289125245337cc46a2606effb7bda", size = 12483136, upload-time = "2026-08-06T10:04:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/63/9e/6fae0ff225a0012642cf72c077e20f8f448c0a80771bc3360e8178fe2f32/ty-0.0.69-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f03359cd8e5c412aa0c181118fa9b9061a4dddaedbb61bac0a424fb0814d402", size = 12799025, upload-time = "2026-08-06T10:04:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/e4/43/78a658d18b2a4ccf35b053392f2213bf12e3c63b2abea512d3b6751d1f4c/ty-0.0.69-py3-none-win32.whl", hash = "sha256:ec460e01586b1eb91894c4a8403bee3e045a47e7a4ada943cc27ce8e348e88cf", size = 11787774, upload-time = "2026-08-06T10:04:18.622Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/88db1f674403f2b81316a853a44a81ed220621fa96f8f7ae586fb6ca7513/ty-0.0.69-py3-none-win_amd64.whl", hash = "sha256:18976ca26a4e28fc3249477f79a695d5502e670803f2e080d89ac905baef3c6e", size = 12864038, upload-time = "2026-08-06T10:04:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/4d/7b/6fc6efd00c69103d70f2bdbe824343089cd70b17b3079170057d3e5a3ac0/ty-0.0.69-py3-none-win_arm64.whl", hash = "sha256:7d4ca3bb74d91cb9947ba3f3b4cb131ad6a2b3ecc76d34040c4ec6092d2e411d", size = 12196693, upload-time = "2026-08-06T10:04:22.902Z" }, ] [[package]]