From ff8872f2c964e80002e3d6f827555aa58e711d7a Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:33:03 -0400 Subject: [PATCH 1/3] fix: handle boolean property schemas in JSON Schema parsing JSON Schema draft-06+ allows `true` and `false` as valid property schemas, meaning "any value valid" and "no value valid" respectively. Fixes #3783. --- src/fastmcp/utilities/json_schema_type.py | 20 +++++- .../json_schema_type/test_json_schema_type.py | 68 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/utilities/json_schema_type.py b/src/fastmcp/utilities/json_schema_type.py index e45bab71a..a975f8e90 100644 --- a/src/fastmcp/utilities/json_schema_type.py +++ b/src/fastmcp/utilities/json_schema_type.py @@ -313,10 +313,16 @@ def _get_from_type_handler( def _schema_to_type( - schema: Mapping[str, Any], + schema: Mapping[str, Any] | bool, schemas: Mapping[str, Any], ) -> type | ForwardRef: """Convert schema to appropriate Python type.""" + # Boolean schemas are valid in JSON Schema draft-06+: + # true means "any value is valid" (equivalent to {}), + # false means "no value is valid". + if isinstance(schema, bool): + return Any + if not schema: return object @@ -475,6 +481,10 @@ def _create_pydantic_model( defaults = {} for prop_name, prop_schema in properties.items(): + # Normalize boolean schemas (JSON Schema draft-06+) + if isinstance(prop_schema, bool): + prop_schema = {} + field_type = _schema_to_type(prop_schema, schemas or {}) # Handle defaults @@ -538,6 +548,10 @@ def _create_dataclass( fields: list[tuple[Any, ...]] = [] for prop_name, prop_schema in properties.items(): + # Normalize boolean schemas (JSON Schema draft-06+) + if isinstance(prop_schema, bool): + prop_schema = {} + field_name = _sanitize_name(prop_name) # Check for self-reference in property @@ -623,6 +637,10 @@ def _merge_defaults( # For each property in the schema for prop_name, prop_schema in schema.get("properties", {}).items(): + # Normalize boolean schemas (JSON Schema draft-06+) + if isinstance(prop_schema, bool): + continue + # If property is missing, apply defaults in priority order if prop_name not in result: if parent_default and prop_name in parent_default: diff --git a/tests/utilities/json_schema_type/test_json_schema_type.py b/tests/utilities/json_schema_type/test_json_schema_type.py index fc2226bdc..6905eed14 100644 --- a/tests/utilities/json_schema_type/test_json_schema_type.py +++ b/tests/utilities/json_schema_type/test_json_schema_type.py @@ -111,6 +111,74 @@ class TestSimpleTypes: validator.validate_python(False) +class TestBooleanSchemas: + """JSON Schema draft-06+ allows true/false as property schemas.""" + + def test_true_property_schema_accepts_any_value(self): + """A property with schema `true` should accept any value.""" + schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "anything": True}, + "required": ["name", "anything"], + } + result = json_schema_to_type(schema) + validator = TypeAdapter(result) + obj = validator.validate_python({"name": "test", "anything": 42}) + assert obj.name == "test" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + assert obj.anything == 42 # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + + def test_false_property_schema_does_not_crash(self): + """A property with schema `false` should not crash parsing.""" + schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "never": False}, + "required": ["name"], + } + result = json_schema_to_type(schema) + validator = TypeAdapter(result) + obj = validator.validate_python({"name": "test"}) + assert obj.name == "test" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + + def test_boolean_schema_in_object_with_additional_properties(self): + """Boolean property schemas work alongside additionalProperties=True.""" + schema = { + "type": "object", + "properties": { + "known": {"type": "string"}, + "flexible": True, + }, + "required": ["known"], + "additionalProperties": True, + } + result = json_schema_to_type(schema) + validator = TypeAdapter(result) + obj = validator.validate_python( + {"known": "hello", "flexible": [1, 2, 3], "extra": "field"} + ) + assert obj.known == "hello" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + assert obj.flexible == [1, 2, 3] # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + + def test_issue_3783_boolean_property_schemas(self): + """Regression test for GitHub issue #3783.""" + schema = { + "type": "object", + "properties": { + "ts": {"type": "integer"}, + "level": True, + "app": True, + "tag": {"type": ["array", "null"], "items": {"type": "string"}}, + }, + "required": ["ts"], + "additionalProperties": True, + } + result = json_schema_to_type(schema) + validator = TypeAdapter(result) + obj = validator.validate_python({"ts": 123, "level": "info", "app": "myapp"}) + assert obj.ts == 123 # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + assert obj.level == "info" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + assert obj.app == "myapp" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + + class TestConstrainedTypes: def test_constant(self): validator = TypeAdapter(Literal["x"]) From d69a86dccfe2a21615d020985ddba0654e4dcf13 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:47:27 -0400 Subject: [PATCH 2/3] fix: enforce false boolean schemas as unsatisfiable `true` returns Any (any value valid), but `false` now returns an unsatisfiable type that rejects all values during Pydantic validation. --- src/fastmcp/utilities/json_schema_type.py | 37 +++++++++++++------ .../json_schema_type/test_json_schema_type.py | 7 +++- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/fastmcp/utilities/json_schema_type.py b/src/fastmcp/utilities/json_schema_type.py index a975f8e90..8b50ff568 100644 --- a/src/fastmcp/utilities/json_schema_type.py +++ b/src/fastmcp/utilities/json_schema_type.py @@ -53,6 +53,7 @@ from typing import ( from pydantic import ( AnyUrl, BaseModel, + BeforeValidator, ConfigDict, EmailStr, Field, @@ -65,6 +66,15 @@ from typing_extensions import NotRequired, TypedDict __all__ = ["JSONSchema", "json_schema_to_type"] +def _reject_all(v: Any) -> Any: + """Validator that rejects every value, implementing JSON Schema `false`.""" + raise ValueError("No value is valid against a false schema") + + +# JSON Schema `false` means no value is valid. This type rejects everything +# during Pydantic validation. +_UnsatisfiableType = Annotated[Any, BeforeValidator(_reject_all)] + FORMAT_TYPES: dict[str, Any] = { "date-time": datetime, "email": EmailStr, @@ -319,9 +329,11 @@ def _schema_to_type( """Convert schema to appropriate Python type.""" # Boolean schemas are valid in JSON Schema draft-06+: # true means "any value is valid" (equivalent to {}), - # false means "no value is valid". - if isinstance(schema, bool): + # false means "no value is valid" (unsatisfiable). + if schema is True: return Any + if schema is False: + return _UnsatisfiableType # type: ignore[return-value] # ty:ignore[invalid-return-type] if not schema: return object @@ -481,11 +493,13 @@ def _create_pydantic_model( defaults = {} for prop_name, prop_schema in properties.items(): - # Normalize boolean schemas (JSON Schema draft-06+) + # Boolean schemas (JSON Schema draft-06+): resolve type directly, + # then use an empty dict for .get() calls below. if isinstance(prop_schema, bool): + field_type = _schema_to_type(prop_schema, schemas or {}) prop_schema = {} - - field_type = _schema_to_type(prop_schema, schemas or {}) + else: + field_type = _schema_to_type(prop_schema, schemas or {}) # Handle defaults default_value = prop_schema.get("default", MISSING) @@ -548,14 +562,15 @@ def _create_dataclass( fields: list[tuple[Any, ...]] = [] for prop_name, prop_schema in properties.items(): - # Normalize boolean schemas (JSON Schema draft-06+) - if isinstance(prop_schema, bool): - prop_schema = {} - field_name = _sanitize_name(prop_name) - # Check for self-reference in property - if prop_schema.get("$ref") == "#": + # Boolean schemas (JSON Schema draft-06+): resolve type directly, + # then use an empty dict for .get() calls below. + if isinstance(prop_schema, bool): + field_type = _schema_to_type(prop_schema, schemas or {}) + prop_schema = {} + elif prop_schema.get("$ref") == "#": + # Check for self-reference in property field_type = ForwardRef(sanitized_name) else: field_type = _schema_to_type(prop_schema, schemas or {}) diff --git a/tests/utilities/json_schema_type/test_json_schema_type.py b/tests/utilities/json_schema_type/test_json_schema_type.py index 6905eed14..ec83f8b88 100644 --- a/tests/utilities/json_schema_type/test_json_schema_type.py +++ b/tests/utilities/json_schema_type/test_json_schema_type.py @@ -127,8 +127,8 @@ class TestBooleanSchemas: assert obj.name == "test" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] assert obj.anything == 42 # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] - def test_false_property_schema_does_not_crash(self): - """A property with schema `false` should not crash parsing.""" + def test_false_property_schema_rejects_values(self): + """A property with schema `false` should reject any provided value.""" schema = { "type": "object", "properties": {"name": {"type": "string"}, "never": False}, @@ -139,6 +139,9 @@ class TestBooleanSchemas: obj = validator.validate_python({"name": "test"}) assert obj.name == "test" # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] + with pytest.raises(ValidationError): + validator.validate_python({"name": "test", "never": "anything"}) + def test_boolean_schema_in_object_with_additional_properties(self): """Boolean property schemas work alongside additionalProperties=True.""" schema = { From 9103eb4f35eb83d589ac4e3052bdd10e7a804fc4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:40:46 -0400 Subject: [PATCH 3/3] fix: resolve ty 0.0.29 type-checking errors Bump ty floor to >=0.0.29 and suppress five new false positives from stricter tuple-union indexing, overload return-type inference, and AsyncBaseTransport attribute resolution. --- pyproject.toml | 2 +- src/fastmcp/server/auth/ssrf.py | 2 +- src/fastmcp/server/context.py | 2 +- tests/client/transports/test_transports.py | 4 +-- tests/server/tasks/test_task_mount.py | 2 +- uv.lock | 38 +++++++++++----------- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index be8794e95..f377800da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ dev = [ "pytest-timeout>=2.4.0", "pytest-xdist>=3.6.1", "ruff>=0.12.8", - "ty>=0.0.26", + "ty>=0.0.29", "prek>=0.2.12", "loq>=0.1.0a3", "opentelemetry-exporter-otlp-proto-grpc>=1.39.0", diff --git a/src/fastmcp/server/auth/ssrf.py b/src/fastmcp/server/auth/ssrf.py index 39c28e959..86ea2031d 100644 --- a/src/fastmcp/server/auth/ssrf.py +++ b/src/fastmcp/server/auth/ssrf.py @@ -119,7 +119,7 @@ async def resolve_hostname(hostname: str, port: int = 443) -> list[str]: ips = list({info[4][0] for info in infos}) if not ips: raise SSRFError(f"DNS resolution returned no addresses for {hostname}") - return ips + return ips # ty: ignore[invalid-return-type] except socket.gaierror as e: raise SSRFError(f"DNS resolution failed for {hostname}: {e}") from e diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 7fe3bb62e..afaae2f5a 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -999,7 +999,7 @@ class Context: session.create_message() API directly. """ # TODO: Add background task support similar to elicit() when is_background_task - return await sample_impl( + return await sample_impl( # ty: ignore[invalid-return-type] self, messages=messages, system_prompt=system_prompt, diff --git a/tests/client/transports/test_transports.py b/tests/client/transports/test_transports.py index b2f319e09..fa9dd710c 100644 --- a/tests/client/transports/test_transports.py +++ b/tests/client/transports/test_transports.py @@ -203,7 +203,7 @@ class TestSSLVerify: assert isinstance(client.transport.auth, OAuth) async with client.transport.auth.httpx_client_factory() as httpx_client: assert ( - httpx_client._transport._pool._ssl_context.verify_mode + httpx_client._transport._pool._ssl_context.verify_mode # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] == VerifyMode.CERT_NONE ) @@ -226,7 +226,7 @@ class TestSSLVerify: assert isinstance(client.transport.auth, OAuth) async with client.transport.auth.httpx_client_factory() as httpx_client: assert ( - httpx_client._transport._pool._ssl_context.verify_mode + httpx_client._transport._pool._ssl_context.verify_mode # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] != VerifyMode.CERT_NONE ) diff --git a/tests/server/tasks/test_task_mount.py b/tests/server/tasks/test_task_mount.py index 03663b07f..b00653ca3 100644 --- a/tests/server/tasks/test_task_mount.py +++ b/tests/server/tasks/test_task_mount.py @@ -583,7 +583,7 @@ class TestMountedTaskMetadata: execution=ToolExecution(taskSupport="optional"), ) - proxy = ProxyTool.from_mcp_tool(lambda: None, mcp_tool) + proxy = ProxyTool.from_mcp_tool(lambda: None, mcp_tool) # ty: ignore[invalid-argument-type] result = proxy.to_mcp_tool(name=proxy.name) assert result.execution is not None diff --git a/uv.lock b/uv.lock index 49a18196a..99450e914 100644 --- a/uv.lock +++ b/uv.lock @@ -921,7 +921,7 @@ dev = [ { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "ruff", specifier = ">=0.12.8" }, - { name = "ty", specifier = ">=0.0.26" }, + { name = "ty", specifier = ">=0.0.29" }, ] [[package]] @@ -2988,26 +2988,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.26" +version = "0.0.29" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/94/4879b81f8681117ccaf31544579304f6dc2ddcc0c67f872afb35869643a2/ty-0.0.26.tar.gz", hash = "sha256:0496b62405d62de7b954d6d677dc1cc5d3046197215d7a0a7fef37745d7b6d29", size = 5393643, upload-time = "2026-03-26T16:27:11.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d5/853561de49fae38c519e905b2d8da9c531219608f1fccc47a0fc2c896980/ty-0.0.29.tar.gz", hash = "sha256:e7936cca2f691eeda631876c92809688dbbab68687c3473f526cd83b6a9228d8", size = 5469221, upload-time = "2026-04-05T15:01:21.328Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/24/99fe33ecd7e16d23c53b0d4244778c6d1b6eb1663b091236dcba22882d67/ty-0.0.26-py3-none-linux_armv6l.whl", hash = "sha256:35beaa56cf59725fd59ab35d8445bbd40b97fe76db39b052b1fcb31f9bf8adf7", size = 10521856, upload-time = "2026-03-26T16:27:06.335Z" }, - { url = "https://files.pythonhosted.org/packages/55/97/1b5e939e2ff69b9bb279ab680bfa8f677d886309a1ac8d9588fd6ce58146/ty-0.0.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:487a0be58ab0eb02e31ba71eb6953812a0f88e50633469b0c0ce3fb795fe0fa1", size = 10320958, upload-time = "2026-03-26T16:27:13.849Z" }, - { url = "https://files.pythonhosted.org/packages/71/25/37081461e13d38a190e5646948d7bc42084f7bd1c6b44f12550be3923e7e/ty-0.0.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a01b7de5693379646d423b68f119719a1338a20017ba48a93eefaff1ee56f97b", size = 9799905, upload-time = "2026-03-26T16:26:55.805Z" }, - { url = "https://files.pythonhosted.org/packages/a1/1c/295d8f55a7b0e037dfc3a5ec4bdda3ab3cbca6f492f725bf269f96a4d841/ty-0.0.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:628c3ee869d113dd2bd249925662fd39d9d0305a6cb38f640ddaa7436b74a1ef", size = 10317507, upload-time = "2026-03-26T16:27:31.887Z" }, - { url = "https://files.pythonhosted.org/packages/1d/62/48b3875c5d2f48fe017468d4bbdde1164c76a8184374f1d5e6162cf7d9b8/ty-0.0.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63d04f35f5370cbc91c0b9675dc83e0c53678125a7b629c9c95769e86f123e65", size = 10319821, upload-time = "2026-03-26T16:27:29.647Z" }, - { url = "https://files.pythonhosted.org/packages/ff/28/cfb2d495046d5bf42d532325cea7412fa1189912d549dbfae417a24fd794/ty-0.0.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a53c4e6f6a91927f8b90e584a4b12bcde05b0c1870ddff8d17462168ad7947a", size = 10831757, upload-time = "2026-03-26T16:27:37.441Z" }, - { url = "https://files.pythonhosted.org/packages/26/bf/dbc3e42f448a2d862651de070b4108028c543ca18cab096b38d7de449915/ty-0.0.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:caf2ced0e58d898d5e3ba5cb843e0ebd377c8a461464748586049afbd9321f51", size = 11369556, upload-time = "2026-03-26T16:26:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/92/4c/6d2f8f34bc6d502ab778c9345a4a936a72ae113de11329c1764bb1f204f6/ty-0.0.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:384807bbcb7d7ce9b97ee5aaa6417a8ae03ccfb426c52b08018ca62cf60f5430", size = 11085679, upload-time = "2026-03-26T16:27:21.746Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f4/f3f61c203bc980dd9bba0ba7ed3c6e81ddfd36b286330f9487c2c7d041aa/ty-0.0.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2c766a94d79b4f82995d41229702caf2d76e5c440ec7e543d05c70e98bf8ab", size = 10900581, upload-time = "2026-03-26T16:27:24.39Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fd/3ca1b4e4bdd129829e9ce78677e0f8e0f1038a7702dccecfa52f037c6046/ty-0.0.26-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f41ac45a0f8e3e8e181508d863a0a62156341db0f624ffd004b97ee550a9de80", size = 10294401, upload-time = "2026-03-26T16:27:03.999Z" }, - { url = "https://files.pythonhosted.org/packages/de/20/4ee3d8c3f90e008843795c765cb8bb245f188c23e5e5cc612c7697406fba/ty-0.0.26-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:73eb8327a34d529438dfe4db46796946c4e825167cbee434dc148569892e435f", size = 10351469, upload-time = "2026-03-26T16:27:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b1/9fb154ade65906d4148f0b999c4a8257c2a34253cb72e15d84c1f04a064e/ty-0.0.26-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4bb53a79259516535a1b55f613ba1619e9c666854946474ca8418c35a5c4fd60", size = 10529488, upload-time = "2026-03-26T16:27:01.378Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b02b03b1862e27b64143db65946d68b138160a5b6bfea193bee0b8bbc34/ty-0.0.26-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f0e75edc1aeb1b4b84af516c7891f631254a4ca3dcd15e848fa1e061e1fe9da", size = 10999015, upload-time = "2026-03-26T16:27:34.636Z" }, - { url = "https://files.pythonhosted.org/packages/21/16/0a56b8667296e2989b9d48095472d98ebf57a0006c71f2a101bbc62a142d/ty-0.0.26-py3-none-win32.whl", hash = "sha256:943c998c5523ed6b519c899c0c39b26b4c751a9759e460fb964765a44cde226f", size = 9912378, upload-time = "2026-03-26T16:27:08.999Z" }, - { url = "https://files.pythonhosted.org/packages/60/c2/fef0d4bba9cd89a82d725b3b1a66efb1b36629ecf0fb1d8e916cb75b8829/ty-0.0.26-py3-none-win_amd64.whl", hash = "sha256:19c856d343efeb1ecad8ee220848f5d2c424daf7b2feda357763ad3036e2172f", size = 10863737, upload-time = "2026-03-26T16:27:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/4d/05/888ebcb3c4d3b6b72d5d3241fddd299142caa3c516e6d26a9cd887dfed3b/ty-0.0.26-py3-none-win_arm64.whl", hash = "sha256:2cde58ccffa046db1223dc28f3e7d4f2c7da8267e97cc5cd186af6fe85f1758a", size = 10285408, upload-time = "2026-03-26T16:27:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/03/b7/911f9962115acfa24e3b2ec9d4992dd994c38e8769e1b1d7680bb4d28a51/ty-0.0.29-py3-none-linux_armv6l.whl", hash = "sha256:b8a40955f7660d3eaceb0d964affc81b790c0765e7052921a5f861ff8a471c30", size = 10568206, upload-time = "2026-04-05T15:01:19.165Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/fcae2167d4c77a97269f92f11d1b43b03617f81de1283d5d05b43432110c/ty-0.0.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6b6849adae15b00bbe2d3c5b078967dcb62eba37d38936b8eeb4c81a82d2e3b8", size = 10442530, upload-time = "2026-04-05T15:01:28.471Z" }, + { url = "https://files.pythonhosted.org/packages/97/33/5a6bfa240cfcb9c36046ae2459fa9ea23238d20130d8656ff5ac4d6c012a/ty-0.0.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dcdd9b17209788152f7b7ea815eda07989152325052fe690013537cc7904ce49", size = 9915735, upload-time = "2026-04-05T15:01:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/318f45fae232118e81a6306c30f50de42c509c412128d5bd231eab699ffb/ty-0.0.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d8ed4789bae78ffaf94462c0d25589a734cab0366b86f2bbcb1bb90e1a7a169", size = 10419748, upload-time = "2026-04-05T15:01:32.375Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a8/5687872e2ab5a0f7dd4fd8456eac31e9381ad4dc74961f6f29965ad4dd91/ty-0.0.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91ec374b8565e0ad0900011c24641ebbef2da51adbd4fb69ff3280c8a7eceb02", size = 10394738, upload-time = "2026-04-05T15:01:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/de/68/015d118097eeb95e6a44c4abce4c0a28b7b9dfb3085b7f0ee48e4f099633/ty-0.0.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298a8d5faa2502d3810bbbb47a030b9455495b9921594206043c785dd61548cf", size = 10910613, upload-time = "2026-04-05T15:01:17.17Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/47ce3c6c53e0670eadbe80756b167bf80ed6681d1ba57cfde2e8065a13d1/ty-0.0.29-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c8fba1a3524c6109d1e020d92301c79d41bf442fa8d335b9fa366239339cb70", size = 11475750, upload-time = "2026-04-05T15:01:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cf/e361845b1081c9264ad5b7c963231bab03f2666865a9f2a115c4233f2137/ty-0.0.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c48adf88a70d264128c39ee922ed14a947817fced1e93c08c1a89c9244edcde", size = 11190055, upload-time = "2026-04-05T15:01:12.369Z" }, + { url = "https://files.pythonhosted.org/packages/79/12/0fb0857e9a62cb11586e9a712103877bbf717f5fb570d16634408cfdefee/ty-0.0.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce0a7a0e96bc7b42518cd3a1a6a6298ef64ff40ca4614355c1aa807059b5c6f", size = 11020539, upload-time = "2026-04-05T15:01:37.022Z" }, + { url = "https://files.pythonhosted.org/packages/20/36/5a26753802083f80cd125db6c4348ad42b3c982ec36e718e0bf4c18f75e5/ty-0.0.29-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6ac86a05b4a3731d45365ab97780acc7b8146fa62fccb3cbe94fe6546c67a97", size = 10396399, upload-time = "2026-04-05T15:01:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/00/e6/b4e75b5752239ab3ab400f19faef4dbef81d05aab5d3419fda0c062a3765/ty-0.0.29-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6bbbf53141af0f3150bf288d716263f1a3550054e4b3551ca866d38192ba9891", size = 10421461, upload-time = "2026-04-05T15:01:08.367Z" }, + { url = "https://files.pythonhosted.org/packages/c0/21/1084b5b609f9abed62070ec0b31c283a403832a6310c8bbc208bd45ee1e6/ty-0.0.29-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1c9e06b770c1d0ff5efc51e34312390db31d53fcf3088163f413030b42b74f84", size = 10599187, upload-time = "2026-04-05T15:01:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a1/ce19a2ca717bbcc1ee11378aba52ef70b6ce5b87245162a729d9fdc2360f/ty-0.0.29-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0307fe37e3f000ef1a4ae230bbaf511508a78d24a5e51b40902a21b09d5e6037", size = 11121198, upload-time = "2026-04-05T15:01:15.22Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6b/f1430b279af704321566ce7ec2725d3d8258c2f815ebd93e474c64cd4543/ty-0.0.29-py3-none-win32.whl", hash = "sha256:7a2a898217960a825f8bc0087e1fdbaf379606175e98f9807187221d53a4a8ed", size = 9995331, upload-time = "2026-04-05T15:01:01.32Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ef/3ef01c17785ff9a69378465c7d0faccd48a07b163554db0995e5d65a5a23/ty-0.0.29-py3-none-win_amd64.whl", hash = "sha256:fc1294200226b91615acbf34e0a9ad81caf98c081e9c6a912a31b0a7b603bc3f", size = 11023644, upload-time = "2026-04-05T15:01:04.432Z" }, + { url = "https://files.pythonhosted.org/packages/2c/55/87280a994d6a2d2647c65e12abbc997ed49835794366153c04c4d9304d76/ty-0.0.29-py3-none-win_arm64.whl", hash = "sha256:f9794bbd1bb3ce13f78c191d0c89ae4c63f52c12b6daa0c6fe220b90d019d12c", size = 10428165, upload-time = "2026-04-05T15:01:34.665Z" }, ] [[package]]