diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e37333440..7b926a9ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,6 +81,10 @@ jobs: uv run --no-project ruff check --output-format=github python/ uv run --no-project ruff format --check python/ + - name: Run ty + run: | + uv run --no-project ty check --output-format github + - name: Run codespell run: | uv run --no-project codespell --toml pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a212480b..9419fc3cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,13 @@ repos: - id: ruff-format - repo: local hooks: + - id: ty + name: ty + description: Type check python/datafusion with ty. + entry: uv run --no-project ty check + pass_filenames: false + types: [file, python] + language: system - id: rust-fmt name: Rust fmt description: Run cargo fmt on files included in the commit. rustfmt should be installed before-hand. diff --git a/pyproject.toml b/pyproject.toml index 604d3f847..b115b1198 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -181,6 +181,16 @@ extend-allowed-calls = ["datafusion.lit", "lit"] # CI and pre-commit invoke codespell with different paths, so we have a little # redundancy here, and we intentionally drop python in the path. +[tool.ty.src] +# Only the published package is type checked for now. Tests and examples +# can be added once the package itself is clean. +include = ["python/datafusion"] + +[tool.ty.analysis] +# `_internal` is the compiled PyO3 extension and ships no `.pyi` stubs. +# pandas and polars are optional and only imported under TYPE_CHECKING. +allowed-unresolved-imports = ["datafusion._internal", "pandas", "polars"] + [tool.codespell] skip = [ "*/tests/test_functions.py", @@ -219,6 +229,8 @@ dev = [ "pyyaml>=6.0.3", "ruff>=0.15.1", "toml>=0.10.2", + # Pinned exactly: ty is pre-1.0 and new releases can add diagnostics. + "ty==0.0.84", ] # Release tooling only. Kept out of `dev` because pygithub pulls in # cryptography, which ships no free-threaded wheel and fails to build diff --git a/python/datafusion/__init__.py b/python/datafusion/__init__.py index 76f647796..087b27672 100644 --- a/python/datafusion/__init__.py +++ b/python/datafusion/__init__.py @@ -57,13 +57,9 @@ from __future__ import annotations +import importlib.metadata as importlib_metadata from typing import Any -try: - import importlib.metadata as importlib_metadata -except ImportError: - import importlib_metadata # type: ignore[import] - # Public submodules from . import functions, ipc, object_store, substrait, unparser diff --git a/python/datafusion/catalog.py b/python/datafusion/catalog.py index d1d501172..9161c62fe 100644 --- a/python/datafusion/catalog.py +++ b/python/datafusion/catalog.py @@ -19,6 +19,7 @@ from __future__ import annotations +import sys from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Protocol @@ -31,10 +32,10 @@ from datafusion.context import TableProviderExportable from datafusion.expr import CreateExternalTable -try: - from warnings import deprecated # Python 3.13+ -except ImportError: - from typing_extensions import deprecated # Python 3.12 +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated __all__ = [ diff --git a/python/datafusion/context.py b/python/datafusion/context.py index c663dea2b..08dbc4400 100644 --- a/python/datafusion/context.py +++ b/python/datafusion/context.py @@ -44,14 +44,15 @@ from __future__ import annotations +import sys import uuid import warnings from typing import TYPE_CHECKING, Any, Protocol -try: - from warnings import deprecated # Python 3.13+ -except ImportError: - from typing_extensions import deprecated # Python 3.12 +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated from urllib.parse import urlparse @@ -85,7 +86,6 @@ if TYPE_CHECKING: import pathlib - import sys from collections.abc import Iterable, Sequence import pandas as pd @@ -1292,7 +1292,7 @@ def register_csv( delimiter=delimiter, schema_infer_max_records=schema_infer_max_records, file_extension=file_extension, - file_compression_type=file_compression_type, + file_compression_type=file_compression_type or "", ) ) @@ -2196,7 +2196,7 @@ def read_csv( schema_infer_max_records=schema_infer_max_records, file_extension=file_extension, table_partition_cols=table_partition_cols, - file_compression_type=file_compression_type, + file_compression_type=file_compression_type or "", ) ) diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py index 8a867a8c6..12ab0a3c3 100644 --- a/python/datafusion/dataframe.py +++ b/python/datafusion/dataframe.py @@ -44,19 +44,21 @@ from __future__ import annotations +import sys import warnings from collections.abc import AsyncIterator, Iterable, Iterator, Sequence from typing import ( TYPE_CHECKING, Any, Literal, + cast, overload, ) -try: - from warnings import deprecated # Python 3.13+ -except ImportError: - from typing_extensions import deprecated # Python 3.12 +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated from datafusion._internal import DataFrame as DataFrameInternal from datafusion._internal import DataFrameWriteOptions as DataFrameWriteOptionsInternal @@ -1158,8 +1160,8 @@ def join( if left_on is not None or right_on is not None: error_msg = "`left_on` or `right_on` should not provided with `on`" raise ValueError(error_msg) - left_on = on - right_on = on + # The legacy ``(left, right)`` tuple form was consumed above. + left_on = right_on = cast("str | Sequence[str]", on) elif left_on is not None or right_on is not None: if left_on is None or right_on is None: error_msg = "`left_on` and `right_on` should both be provided." @@ -1344,10 +1346,11 @@ def repartition_by_hash(self, *exprs: Expr | str, num: int) -> DataFrame: Returns: Repartitioned DataFrame. """ - exprs = [self.parse_sql_expr(e) if isinstance(e, str) else e for e in exprs] - exprs = expr_list_to_raw_expr_list(exprs) + raw_exprs = expr_list_to_raw_expr_list( + [self.parse_sql_expr(e) if isinstance(e, str) else e for e in exprs] + ) - return DataFrame(self.df.repartition_by_hash(*exprs, num=num)) + return DataFrame(self.df.repartition_by_hash(*raw_exprs, num=num)) def union(self, other: DataFrame, distinct: bool = False) -> DataFrame: """Calculate the union of two :py:class:`DataFrame`. @@ -1833,10 +1836,9 @@ def unnest_columns( >>> df.unnest_columns("a", recursions=[("a", "a", 1)]).to_pydict() {'a': [1, 2, 3], 'b': ['x', 'x', 'y']} """ - columns = list(columns) return DataFrame( self.df.unnest_columns( - columns, preserve_nulls=preserve_nulls, recursions=recursions + list(columns), preserve_nulls=preserve_nulls, recursions=recursions ) ) diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py index bfa124ff0..d6cf85bc1 100644 --- a/python/datafusion/expr.py +++ b/python/datafusion/expr.py @@ -46,13 +46,14 @@ from __future__ import annotations +import sys from collections.abc import Callable, Iterable, Sequence -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, overload -try: - from warnings import deprecated # Python 3.13+ -except ImportError: - from typing_extensions import deprecated # Python 3.12 +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated import pyarrow as pa @@ -114,7 +115,7 @@ def _create_external_table_location(self: Any) -> str: return locations[0] if locations else "" -CreateExternalTable.location = _create_external_table_location +CreateExternalTable.location = _create_external_table_location # ty: ignore[deprecated] CreateFunction = expr_internal.CreateFunction CreateFunctionBody = expr_internal.CreateFunctionBody @@ -410,8 +411,18 @@ def _to_raw_expr(value: Expr | str) -> expr_internal.Expr: raise TypeError(error) +@overload +def expr_list_to_raw_expr_list(expr_list: None) -> None: ... + + +@overload +def expr_list_to_raw_expr_list( + expr_list: Sequence[Expr | str] | Expr | str, +) -> list[expr_internal.Expr]: ... + + def expr_list_to_raw_expr_list( - expr_list: list[Expr] | Expr | None, + expr_list: Sequence[Expr | str] | Expr | str | None, ) -> list[expr_internal.Expr] | None: """Convert a sequence of expressions or column names to raw expressions.""" if isinstance(expr_list, Expr | str): @@ -428,6 +439,16 @@ def sort_or_default(e: Expr | SortExpr) -> expr_internal.SortExpr: return SortExpr(e, ascending=True, nulls_first=False).raw_sort +@overload +def sort_list_to_raw_sort_list(sort_list: None) -> None: ... + + +@overload +def sort_list_to_raw_sort_list( + sort_list: Sequence[SortKey] | SortKey, +) -> list[expr_internal.SortExpr]: ... + + def sort_list_to_raw_sort_list( sort_list: Sequence[SortKey] | SortKey | None, ) -> list[expr_internal.SortExpr] | None: @@ -771,7 +792,7 @@ def __getitem__(self, key: str | int) -> Expr: return Expr(functions_internal.array_slice(self.expr, start, stop, step)) return Expr(self.expr.__getitem__(key)) - def __eq__(self, rhs: object) -> Expr: + def __eq__(self, rhs: object) -> Expr: # ty: ignore[invalid-method-override] """Equal to. Accepts either an expression or any valid PyArrow scalar literal value. @@ -782,7 +803,7 @@ def __eq__(self, rhs: object) -> Expr: rhs = Expr.literal(rhs) return Expr(self.expr.__eq__(rhs.expr)) - def __ne__(self, rhs: object) -> Expr: + def __ne__(self, rhs: object) -> Expr: # ty: ignore[invalid-method-override] """Not equal to. Accepts either an expression or any valid PyArrow scalar literal value. diff --git a/python/datafusion/functions/__init__.py b/python/datafusion/functions/__init__.py index d5e146bd5..c7d99f786 100644 --- a/python/datafusion/functions/__init__.py +++ b/python/datafusion/functions/__init__.py @@ -871,8 +871,8 @@ def concat(*args: Expr) -> Expr: >>> result.collect_column("c")[0].as_py() 'hello world' """ - args = [arg.expr for arg in args] - return Expr(f.concat(args)) + raw_args = [arg.expr for arg in args] + return Expr(f.concat(raw_args)) def concat_ws(separator: str, *args: Expr) -> Expr: @@ -888,8 +888,8 @@ def concat_ws(separator: str, *args: Expr) -> Expr: >>> result.collect_column("c")[0].as_py() 'hello-world' """ - args = [arg.expr for arg in args] - return Expr(f.concat_ws(separator, args)) + raw_args = [arg.expr for arg in args] + return Expr(f.concat_ws(separator, raw_args)) def order_by(expr: Expr, ascending: bool = True, nulls_first: bool = False) -> SortExpr: @@ -1272,8 +1272,8 @@ def coalesce(*args: Expr) -> Expr: >>> result.collect_column("c")[0].as_py() 2 """ - args = [arg.expr for arg in args] - return Expr(f.coalesce(*args)) + raw_args = [arg.expr for arg in args] + return Expr(f.coalesce(*raw_args)) def cos(arg: Expr) -> Expr: @@ -3105,8 +3105,8 @@ def make_array(*args: Expr) -> Expr: >>> result.collect_column("arr")[0].as_py() [1, 2, 3] """ - args = [arg.expr for arg in args] - return Expr(f.make_array(args)) + raw_args = [arg.expr for arg in args] + return Expr(f.make_array(raw_args)) def make_list(*args: Expr) -> Expr: @@ -3240,8 +3240,8 @@ def struct(*args: Expr) -> Expr: >>> result.collect_column("s")[0].as_py() == {"c0": 1, "c1": 2} True """ - args = [arg.expr for arg in args] - return Expr(f.struct(*args)) + raw_args = [arg.expr for arg in args] + return Expr(f.struct(*raw_args)) def named_struct(name_pairs: list[tuple[str, Expr]]) -> Expr: @@ -3762,8 +3762,8 @@ def array_concat(*args: Expr) -> Expr: >>> result.collect_column("result")[0].as_py() [1, 2, 3, 4] """ - args = [arg.expr for arg in args] - return Expr(f.array_concat(args)) + raw_args = [arg.expr for arg in args] + return Expr(f.array_concat(raw_args)) def array_cat(*args: Expr) -> Expr: diff --git a/python/datafusion/functions/spark.py b/python/datafusion/functions/spark.py index c4002670d..74315fb40 100644 --- a/python/datafusion/functions/spark.py +++ b/python/datafusion/functions/spark.py @@ -56,14 +56,14 @@ def _filter_raw(filter: Expr | None) -> Any: return filter.expr if filter is not None else None -def _coerce_i32(value: Expr | int | None) -> Expr | None: - """Coerce a native ``int`` to an int32 literal, passing ``Expr``/``None`` through. +def _coerce_i32(value: Expr | int) -> Expr: + """Coerce a native ``int`` to an int32 literal, passing ``Expr`` through. Several Spark datetime and interval builders require 32-bit integer inputs, so a bare ``int`` must become an int32 literal rather than the int64 default that :meth:`Expr.literal` would produce. """ - if value is None or isinstance(value, Expr): + if isinstance(value, Expr): return value return Expr.literal(pa.scalar(value, type=pa.int32())) diff --git a/python/datafusion/plan.py b/python/datafusion/plan.py index 8b61c0979..ffad3862d 100644 --- a/python/datafusion/plan.py +++ b/python/datafusion/plan.py @@ -143,7 +143,7 @@ def to_proto(self) -> bytes: ) return self.to_bytes() - def __eq__(self, other: LogicalPlan) -> bool: + def __eq__(self, other: object) -> bool: """Test equality.""" if not isinstance(other, LogicalPlan): return False diff --git a/python/datafusion/user_defined.py b/python/datafusion/user_defined.py index 1d8ccbe3f..c2624f613 100644 --- a/python/datafusion/user_defined.py +++ b/python/datafusion/user_defined.py @@ -197,6 +197,7 @@ class ScalarUDF: operating on a group of rows. """ + @overload def __init__( self, name: str, @@ -204,6 +205,25 @@ def __init__( input_fields: list[pa.Field], return_field: pa.Field, volatility: Volatility | str, + ) -> None: ... + + @overload + def __init__( + self, + name: str, + func: ScalarUDFExportable, + input_fields: None = ..., + return_field: None = ..., + volatility: None = ..., + ) -> None: ... + + def __init__( + self, + name: str, + func: Callable[..., _R] | ScalarUDFExportable, + input_fields: list[pa.Field] | None = None, + return_field: pa.Field | None = None, + volatility: Volatility | str | None = None, ) -> None: """Instantiate a scalar user-defined function (UDF). @@ -212,6 +232,12 @@ def __init__( if hasattr(func, "__datafusion_scalar_udf__"): self._udf = df_internal.ScalarUDF.from_pycapsule(func) return + if input_fields is None or return_field is None or volatility is None: + msg = ( + "`input_fields`, `return_field`, and `volatility` " + "must be provided when `func` is callable." + ) + raise TypeError(msg) if isinstance(input_fields, pa.DataType): input_fields = [input_fields] self._udf = df_internal.ScalarUDF( @@ -294,7 +320,7 @@ def udf(func: ScalarUDFExportable) -> ScalarUDF: ... def udf(func: _PyCapsule) -> ScalarUDF: ... @staticmethod - def udf(*args: Any, **kwargs: Any): # noqa: D417, C901 + def udf(*args: Any, **kwargs: Any): # noqa: D417 """Create a new User-Defined Function (UDF). This class can be used both as either a function or a decorator. @@ -365,10 +391,7 @@ def _function( msg = "`func` argument must be callable" raise TypeError(msg) if name is None: - if hasattr(func, "__qualname__"): - name = func.__qualname__.lower() - else: - name = func.__class__.__name__.lower() + name = getattr(func, "__qualname__", func.__class__.__name__).lower() input_fields = data_types_or_fields_to_field_list(input_fields) return_field = data_type_or_field_to_field(return_field, "value") return ScalarUDF( @@ -391,7 +414,7 @@ def decorator(func: Callable) -> Callable: ) @functools.wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Callable: + def wrapper(*args: Any, **kwargs: Any) -> Expr: return udf_caller(*args, **kwargs) return wrapper @@ -509,10 +532,10 @@ def __init__( self, name: str, accumulator: Callable[[], Accumulator] | AggregateUDFExportable, - input_types: list[pa.DataType] | None, - return_type: pa.DataType | None, - state_type: list[pa.DataType] | None, - volatility: Volatility | str | None, + input_types: list[pa.DataType] | None = None, + return_type: pa.DataType | None = None, + state_type: list[pa.DataType] | None = None, + volatility: Volatility | str | None = None, ) -> None: """Instantiate a user-defined aggregate function (UDAF). @@ -959,6 +982,7 @@ class WindowUDF: also :py:class:`ScalarUDF` for operating on a row by row basis. """ + @overload def __init__( self, name: str, @@ -966,6 +990,25 @@ def __init__( input_types: list[pa.DataType], return_type: pa.DataType, volatility: Volatility | str, + ) -> None: ... + + @overload + def __init__( + self, + name: str, + func: WindowUDFExportable, + input_types: None = ..., + return_type: None = ..., + volatility: None = ..., + ) -> None: ... + + def __init__( + self, + name: str, + func: Callable[[], WindowEvaluator] | WindowUDFExportable, + input_types: list[pa.DataType] | None = None, + return_type: pa.DataType | None = None, + volatility: Volatility | str | None = None, ) -> None: """Instantiate a user-defined window function (UDWF). @@ -975,6 +1018,12 @@ def __init__( if hasattr(func, "__datafusion_window_udf__"): self._udwf = df_internal.WindowUDF.from_pycapsule(func) return + if input_types is None or return_type is None or volatility is None: + msg = ( + "`input_types`, `return_type`, and `volatility` " + "must be provided when `func` is callable." + ) + raise TypeError(msg) self._udwf = df_internal.WindowUDF( name, func, input_types, return_type, str(volatility) ) @@ -1134,7 +1183,7 @@ def _create_window_udf( msg = "`func` must implement the abstract base class WindowEvaluator" raise TypeError(msg) - name = name or func.__qualname__.lower() + name = name or WindowUDF._get_default_name(func) input_types = ( [input_types] if isinstance(input_types, pa.DataType) else input_types ) @@ -1144,9 +1193,7 @@ def _create_window_udf( @staticmethod def _get_default_name(func: Callable) -> str: """Get the default name for a function based on its attributes.""" - if hasattr(func, "__qualname__"): - return func.__qualname__.lower() - return func.__class__.__name__.lower() + return getattr(func, "__qualname__", func.__class__.__name__).lower() @staticmethod def _normalize_input_types( @@ -1333,7 +1380,7 @@ def _create_table_udf( @staticmethod def _create_table_udf_decorator( - name: str | None = None, + name: str, *, with_session: bool = False, ) -> Callable[[Callable[..., Any]], TableFunction]: diff --git a/uv.lock b/uv.lock index 499829710..b1bf3c5e6 100644 --- a/uv.lock +++ b/uv.lock @@ -505,6 +505,7 @@ dev = [ { name = "pyyaml" }, { name = "ruff" }, { name = "toml" }, + { name = "ty" }, ] docs = [ { name = "jinja2" }, @@ -549,6 +550,7 @@ dev = [ { name = "pyyaml", specifier = ">=6.0.3" }, { name = "ruff", specifier = ">=0.15.1" }, { name = "toml", specifier = ">=0.10.2" }, + { name = "ty", specifier = "==0.0.84" }, ] docs = [ { name = "jinja2", specifier = ">=3.1.5" }, @@ -2937,6 +2939,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] +[[package]] +name = "ty" +version = "0.0.84" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" }, + { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" }, + { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" }, + { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"