diff --git a/CHANGELOG.md b/CHANGELOG.md index aabe40da..eca39bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ chronological order. Releases follow [semantic versioning](https://semver.org/) releases are available on [PyPI](https://pypi.org/project/pytask) and [Anaconda.org](https://anaconda.org/conda-forge/pytask). +## Unreleased + +- [#889](https://github.com/pytask-dev/pytask/pull/889) improves typing for tree + operations by wrapping optree's pytree utilities with pytask-specific signatures + and requiring optree 0.16.0 or newer. + ## 0.6.0 - 2026-05-01 - [#875](https://github.com/pytask-dev/pytask/pull/875) improves the documentation diff --git a/pyproject.toml b/pyproject.toml index e1769eca..03dd7ede 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "click>=8.1.8,!=8.2.0", "click-default-group>=1.2.4", "msgspec>=0.18.6", - "optree>=0.9.0", + "optree>=0.16.0", "packaging>=23.0.0", "pluggy>=1.3.0", "rich>=13.8.0", diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index 15ec9263..b850bf34 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -12,11 +12,11 @@ from _pytask._inspect import get_annotations from _pytask.exceptions import NodeNotCollectedError from _pytask.models import NodeInfo +from _pytask.node_protocols import NodeTree from _pytask.node_protocols import PNode from _pytask.node_protocols import PProvisionalNode from _pytask.nodes import PythonNode from _pytask.task_utils import parse_keyword_arguments_from_signature_defaults -from _pytask.tree_util import PyTree from _pytask.tree_util import tree_leaves from _pytask.tree_util import tree_map_with_path from _pytask.typing import ProductType @@ -254,7 +254,7 @@ def _collect_nodes_and_provisional_nodes( # noqa: PLR0913 task_path: Path | None, parameter_name: str, value: Any, -) -> PyTree[PProvisionalNode | PNode]: +) -> NodeTree: return tree_map_with_path( lambda p, x: collection_func( session, diff --git a/src/_pytask/node_protocols.py b/src/_pytask/node_protocols.py index 27fadd30..b3ec4612 100644 --- a/src/_pytask/node_protocols.py +++ b/src/_pytask/node_protocols.py @@ -3,18 +3,29 @@ from typing import TYPE_CHECKING from typing import Any from typing import Protocol +from typing import TypeAlias from typing import runtime_checkable +from _pytask.tree_util import PyTree + if TYPE_CHECKING: from collections.abc import Callable from pathlib import Path from _pytask.mark import Mark - from _pytask.tree_util import PyTree from _pytask.typing import NodePath -__all__ = ["PNode", "PPathNode", "PProvisionalNode", "PTask", "PTaskWithPath"] +__all__ = [ + "NodeTree", + "PNode", + "PPathNode", + "PProvisionalNode", + "PTask", + "PTaskWithPath", + "TaskIO", + "TaskNode", +] @runtime_checkable @@ -64,45 +75,6 @@ class PPathNode(PNode, Protocol): path: NodePath -@runtime_checkable -class PTask(Protocol): - """Protocol for nodes.""" - - name: str - depends_on: dict[str, PyTree[PNode | PProvisionalNode]] - produces: dict[str, PyTree[PNode | PProvisionalNode]] - function: Callable[..., Any] - markers: list[Mark] - report_sections: list[tuple[str, str, str]] - attributes: dict[Any, Any] - - @property - def signature(self) -> str: - """Return the signature of the node.""" - - def state(self) -> str | None: - """Return the state of the node. - - The state can be something like a hash or a last modified timestamp. If the node - does not exist, you can also return ``None``. - - """ - - def execute(self, **kwargs: Any) -> Any: - """Return the value of the node that will be injected into the task.""" - - -@runtime_checkable -class PTaskWithPath(PTask, Protocol): - """Tasks with paths. - - Tasks with paths receive special handling when it comes to printing their names. - - """ - - path: Path - - @runtime_checkable class PProvisionalNode(Protocol): """A protocol for provisional nodes. @@ -141,3 +113,52 @@ def load(self, is_product: bool = False) -> Any: # pragma: no cover def collect(self) -> list[Any]: """Collect the objects that are defined by the provisional nodes.""" + + +TaskNode: TypeAlias = PNode | PProvisionalNode +"""A concrete or provisional pytask node.""" + +NodeTree: TypeAlias = PyTree[TaskNode] +"""A pytask tree whose leaves are concrete or provisional nodes.""" + +TaskIO: TypeAlias = dict[str, NodeTree] +"""The top-level task argument mapping for dependencies and products.""" + + +@runtime_checkable +class PTask(Protocol): + """Protocol for nodes.""" + + name: str + depends_on: TaskIO + produces: TaskIO + function: Callable[..., Any] + markers: list[Mark] + report_sections: list[tuple[str, str, str]] + attributes: dict[Any, Any] + + @property + def signature(self) -> str: + """Return the signature of the node.""" + + def state(self) -> str | None: + """Return the state of the node. + + The state can be something like a hash or a last modified timestamp. If the node + does not exist, you can also return ``None``. + + """ + + def execute(self, **kwargs: Any) -> Any: + """Return the value of the node that will be injected into the task.""" + + +@runtime_checkable +class PTaskWithPath(PTask, Protocol): + """Tasks with paths. + + Tasks with paths receive special handling when it comes to printing their names. + + """ + + path: Path diff --git a/src/_pytask/nodes.py b/src/_pytask/nodes.py index 8910d289..a4e726ff 100644 --- a/src/_pytask/nodes.py +++ b/src/_pytask/nodes.py @@ -22,6 +22,7 @@ from _pytask.node_protocols import PProvisionalNode from _pytask.node_protocols import PTask from _pytask.node_protocols import PTaskWithPath +from _pytask.node_protocols import TaskIO from _pytask.path import hash_path from _pytask.typing import NoDefault from _pytask.typing import NodePath @@ -34,7 +35,6 @@ from _pytask.mark import Mark from _pytask.models import NodeInfo - from _pytask.tree_util import PyTree __all__ = [ @@ -77,10 +77,8 @@ class TaskWithoutPath(PTask): name: str function: Callable[..., Any] - depends_on: dict[str, PyTree[PNode | PProvisionalNode]] = field( - default_factory=dict - ) - produces: dict[str, PyTree[PNode | PProvisionalNode]] = field(default_factory=dict) + depends_on: TaskIO = field(default_factory=dict) + produces: TaskIO = field(default_factory=dict) markers: list[Mark] = field(default_factory=list) report_sections: list[tuple[str, str, str]] = field(default_factory=list) attributes: dict[Any, Any] = field(default_factory=dict) @@ -133,10 +131,8 @@ class Task(PTaskWithPath): path: Path function: Callable[..., Any] name: str = field(default="", init=False) - depends_on: dict[str, PyTree[PNode | PProvisionalNode]] = field( - default_factory=dict - ) - produces: dict[str, PyTree[PNode | PProvisionalNode]] = field(default_factory=dict) + depends_on: TaskIO = field(default_factory=dict) + produces: TaskIO = field(default_factory=dict) markers: list[Mark] = field(default_factory=list) report_sections: list[tuple[str, str, str]] = field(default_factory=list) attributes: dict[Any, Any] = field(default_factory=dict) diff --git a/src/_pytask/provisional_utils.py b/src/_pytask/provisional_utils.py index 32271016..8522c306 100644 --- a/src/_pytask/provisional_utils.py +++ b/src/_pytask/provisional_utils.py @@ -10,13 +10,12 @@ from _pytask.collect_utils import collect_dependency from _pytask.dag import create_dag_from_session from _pytask.models import NodeInfo -from _pytask.node_protocols import PNode +from _pytask.node_protocols import NodeTree from _pytask.node_protocols import PProvisionalNode from _pytask.node_protocols import PTask from _pytask.node_protocols import PTaskWithPath from _pytask.nodes import Task from _pytask.reports import ExecutionReport -from _pytask.tree_util import PyTree from _pytask.tree_util import tree_map_with_path from _pytask.typing import is_task_generator @@ -29,7 +28,7 @@ def collect_provisional_nodes( session: Session, task: PTask, node: Any, path: tuple[Any, ...] -) -> PyTree[PNode | PProvisionalNode]: +) -> NodeTree: """Collect provisional nodes. 1. Call the [`pytask.PProvisionalNode.collect`][] to receive the raw nodes. diff --git a/src/_pytask/tree_util.py b/src/_pytask/tree_util.py index ee8d137a..d5ee0d7e 100644 --- a/src/_pytask/tree_util.py +++ b/src/_pytask/tree_util.py @@ -2,18 +2,18 @@ from __future__ import annotations -import functools +import typing # noqa: F401 # Needed to resolve optree.PyTree forward refs. +from collections.abc import Callable # noqa: TC003 # Public annotations need it. from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from typing import TypeAlias from typing import TypeVar +from typing import cast +from typing import overload import optree -from optree import tree_flatten_with_path as _optree_tree_flatten_with_path -from optree import tree_leaves as _optree_tree_leaves -from optree import tree_map as _optree_tree_map -from optree import tree_map_with_path as _optree_tree_map_with_path -from optree import tree_structure as _optree_tree_structure +from optree import PyTreeSpec __all__ = [ "TREE_UTIL_LIB_DIRECTORY", @@ -26,13 +26,15 @@ ] _T = TypeVar("_T") +_U = TypeVar("_U") +_K = TypeVar("_K") if TYPE_CHECKING: # Use our own recursive type alias for static type checking. # optree's PyTree uses __class_getitem__ to generate Union types at runtime, # but type checkers like ty cannot evaluate these dynamic types properly. # See: https://github.com/metaopt/optree/issues/251 - PyTree = ( + PyTree: TypeAlias = ( _T | tuple["PyTree[_T]", ...] | list["PyTree[_T]"] | dict[Any, "PyTree[_T]"] ) else: @@ -41,17 +43,219 @@ assert optree.__file__ is not None TREE_UTIL_LIB_DIRECTORY = Path(optree.__file__).parent +_pytree = optree.pytree.reexport(namespace="pytask") -tree_leaves = functools.partial( - _optree_tree_leaves, none_is_leaf=True, namespace="pytask" -) -tree_map = functools.partial(_optree_tree_map, none_is_leaf=True, namespace="pytask") -tree_map_with_path = functools.partial( - _optree_tree_map_with_path, none_is_leaf=True, namespace="pytask" -) -tree_structure = functools.partial( - _optree_tree_structure, none_is_leaf=True, namespace="pytask" -) -tree_flatten_with_path = functools.partial( - _optree_tree_flatten_with_path, none_is_leaf=True, namespace="pytask" -) + +def tree_leaves( + tree: PyTree[_T], + /, + is_leaf: Callable[[Any], bool] | None = None, +) -> list[_T]: + """Return the leaves of a pytask tree.""" + return cast( + "list[_T]", + _pytree.leaves( + cast("Any", tree), + is_leaf, + none_is_leaf=True, + ), + ) + + +@overload +def tree_map( + func: Callable[..., PyTree[_T]], + tree: dict[_K, PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> dict[_K, PyTree[_T]]: ... + + +@overload +def tree_map( + func: Callable[..., _U], + tree: dict[_K, PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> dict[_K, PyTree[_U]]: ... + + +@overload +def tree_map( + func: Callable[..., PyTree[_T]], + tree: list[PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> list[PyTree[_T]]: ... + + +@overload +def tree_map( + func: Callable[..., _U], + tree: list[PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> list[PyTree[_U]]: ... + + +@overload +def tree_map( + func: Callable[..., PyTree[_T]], + tree: tuple[PyTree[_T], ...], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> tuple[PyTree[_T], ...]: ... + + +@overload +def tree_map( + func: Callable[..., _U], + tree: tuple[PyTree[_T], ...], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> tuple[PyTree[_U], ...]: ... + + +@overload +def tree_map( + func: Callable[..., _U], + tree: PyTree[_T], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> PyTree[_U]: ... + + +def tree_map( + func: Callable[..., Any], + tree: PyTree[Any], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> Any: + """Apply a function to each leaf of a pytask tree.""" + return _pytree.map( + func, + cast("Any", tree), + *cast("tuple[Any, ...]", rests), + is_leaf=is_leaf, + none_is_leaf=True, + ) + + +@overload +def tree_map_with_path( + func: Callable[..., PyTree[_T]], + tree: dict[_K, PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> dict[_K, PyTree[_T]]: ... + + +@overload +def tree_map_with_path( + func: Callable[..., _U], + tree: dict[_K, PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> dict[_K, PyTree[_U]]: ... + + +@overload +def tree_map_with_path( + func: Callable[..., PyTree[_T]], + tree: list[PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> list[PyTree[_T]]: ... + + +@overload +def tree_map_with_path( + func: Callable[..., _U], + tree: list[PyTree[_T]], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> list[PyTree[_U]]: ... + + +@overload +def tree_map_with_path( + func: Callable[..., PyTree[_T]], + tree: tuple[PyTree[_T], ...], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> tuple[PyTree[_T], ...]: ... + + +@overload +def tree_map_with_path( + func: Callable[..., _U], + tree: tuple[PyTree[_T], ...], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> tuple[PyTree[_U], ...]: ... + + +@overload +def tree_map_with_path( + func: Callable[..., _U], + tree: PyTree[_T], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> PyTree[_U]: ... + + +def tree_map_with_path( + func: Callable[..., Any], + tree: PyTree[Any], + /, + *rests: PyTree[Any], + is_leaf: Callable[[Any], bool] | None = None, +) -> Any: + """Apply a function to each leaf and pass the leaf path.""" + return _pytree.map_with_path( + func, + cast("Any", tree), + *cast("tuple[Any, ...]", rests), + is_leaf=is_leaf, + none_is_leaf=True, + ) + + +def tree_structure( + tree: PyTree[Any], + /, + is_leaf: Callable[[Any], bool] | None = None, +) -> PyTreeSpec: + """Return the tree structure of a pytask tree.""" + return _pytree.structure(cast("Any", tree), is_leaf, none_is_leaf=True) + + +def tree_flatten_with_path( + tree: PyTree[_T], + /, + is_leaf: Callable[[Any], bool] | None = None, +) -> tuple[list[tuple[Any, ...]], list[_T], PyTreeSpec]: + """Flatten a pytask tree into paths, leaves, and structure.""" + return cast( + "tuple[list[tuple[Any, ...]], list[_T], PyTreeSpec]", + _pytree.flatten_with_path( + cast("Any", tree), + is_leaf, + none_is_leaf=True, + ), + ) diff --git a/uv.lock b/uv.lock index cfddea4f..b8c82f43 100644 --- a/uv.lock +++ b/uv.lock @@ -3028,7 +3028,7 @@ requires-dist = [ { name = "msgspec", specifier = ">=0.18.6" }, { name = "msgspec", extras = ["toml"], specifier = ">=0.18.6" }, { name = "networkx", marker = "extra == 'dag'", specifier = ">=2.4.0" }, - { name = "optree", specifier = ">=0.9.0" }, + { name = "optree", specifier = ">=0.16.0" }, { name = "packaging", specifier = ">=23.0.0" }, { name = "pluggy", specifier = ">=1.3.0" }, { name = "rich", specifier = ">=13.8.0" }, @@ -3928,26 +3928,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.33" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/44/9478c50c266826c1bf30d1692e589755bffa8f1c0a3eb7af8a346c255991/ty-0.0.33.tar.gz", hash = "sha256:46d63bda07403322cb6c28ccfdd5536be916e13df725c29f7ccd0a21f06bd9e8", size = 5559373, upload-time = "2026-04-28T10:45:13.18Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/24/e287388c63a19191be26b32ff4dbd06029834068150ebe2532939bc4c851/ty-0.0.33-py3-none-linux_armv6l.whl", hash = "sha256:94d0a9d2234261a8911396d59e506b5923fe0971dbda43b9dcea287936887fcc", size = 11021308, upload-time = "2026-04-28T10:45:43.34Z" }, - { url = "https://files.pythonhosted.org/packages/00/ca/ba1eed819895bd239fba8ee35dfcd5fcb266c203b0914a17a59579096bb5/ty-0.0.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4a2b5ba078f90de342f56b5f7979bb77c9b9b1d8625a041352ffc6ee93c4073", size = 10777272, upload-time = "2026-04-28T10:45:32.905Z" }, - { url = "https://files.pythonhosted.org/packages/25/a8/c3131d37b44b3fea1d6654a1c929a0cd0873822f77a90482b8ec28f6fbbd/ty-0.0.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84ff5707825e9af9668d2bcf66975f93e520a63b524ab494e3a8265735be2563", size = 10201078, upload-time = "2026-04-28T10:45:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/7b/db/d8e37ff0045810cc65e1ff36aa0da0a2253c05659787ac987df8a16c7897/ty-0.0.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e375285736f57886868e7af0b11c7b0ec5b6543fa15e7ad2a714fed9f077d4e0", size = 10732347, upload-time = "2026-04-28T10:45:21.444Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1a/20e83a412506a918e4684fc67b567cf7cc13b105470b3428cb23c3d5aa13/ty-0.0.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5680f6350c3b4e46b8bff6d7bb132366ea239463d6cad4892725d06046e65464", size = 10808238, upload-time = "2026-04-28T10:45:38.565Z" }, - { url = "https://files.pythonhosted.org/packages/5d/4b/d0a39f4464dc6cb4cc2c159473ce216bd1846bfb684c0323a3cb36dce5c6/ty-0.0.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5535538bad8d0f7e62bcdff02197cdb30e41451d80b35d27e17d128f2e1dc5d", size = 11288348, upload-time = "2026-04-28T10:45:08.419Z" }, - { url = "https://files.pythonhosted.org/packages/35/7e/f1745e0f9583363d7a83d9a4990fc244f76ecc30840ddad83dc16a33c52d/ty-0.0.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da196c42bbbc069e1e21e3e52107c061aa9660352dae57a41930690b56e2c02d", size = 11789907, upload-time = "2026-04-28T10:45:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a5/71/25f39f46a12d662859d45bc648555d0661044eb43db6b5648c9947487da9/ty-0.0.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9281672921ef6d4460e03146b5e6c18cb1a3e3a3b8a1a88f6f33226d05a469b7", size = 11500774, upload-time = "2026-04-28T10:45:48.012Z" }, - { url = "https://files.pythonhosted.org/packages/94/ec/136959ecbb7c71cb90537f5aea441c73f4ab24612868a6ecdc9d7444d32d/ty-0.0.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1b8f303f82da64e878108e764be3ecbcd7c9903ac0a7f7031614ed00b97ab", size = 11360314, upload-time = "2026-04-28T10:45:05.402Z" }, - { url = "https://files.pythonhosted.org/packages/cf/95/32809575c222f00beed498cb728e9290a0f5009f930025381bb7253b2206/ty-0.0.33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:efe3af412c9ff67bce5fa37d0a2b0d8555c24072b145a5bac6c79637f1c83abe", size = 10707785, upload-time = "2026-04-28T10:45:10.836Z" }, - { url = "https://files.pythonhosted.org/packages/13/89/c8e9531f7aa4a093359e15fa32c8e1277fbbe90d16894d7c6032d29f4b34/ty-0.0.33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aeec29c91ea768601747da546c3efc20b72c2fb1bd52bcc786a5c6eeff51d27b", size = 10834987, upload-time = "2026-04-28T10:45:40.738Z" }, - { url = "https://files.pythonhosted.org/packages/31/16/9835fbcf5338af1a1917bd28fdb8a7193c210b83f243aa286fa9f79cb3ad/ty-0.0.33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a535977c52bbb5f7e96b8b70a6ad375ad077f4a9ff2492508ea3816a2b403819", size = 10968968, upload-time = "2026-04-28T10:45:30.26Z" }, - { url = "https://files.pythonhosted.org/packages/36/69/64c76aabc1bc70c7f24b686cd93c3407f8ea430905e395f59bf9603ef571/ty-0.0.33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d732facf39fcb221ba279d469c5040d37883e964f123b1563888efd34818180", size = 11458077, upload-time = "2026-04-28T10:45:45.971Z" }, - { url = "https://files.pythonhosted.org/packages/91/84/fae27b0c4718776a298690d31ca4cc1995f2e3e1c63a7b59e84c41498e9a/ty-0.0.33-py3-none-win32.whl", hash = "sha256:d90960b574428dc252f85e8598ec5fcb7f619794196b2fc95a90da075ed4681c", size = 10345364, upload-time = "2026-04-28T10:45:16.836Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a0/a2938b23ae3e1a09a2d7c189e2ac5f7113676bae4e0e23948b568e18e5f8/ty-0.0.33-py3-none-win_amd64.whl", hash = "sha256:c1c3aec62c44de610c6e95f0a4e97ac3dbc07934bfdbf1fd90d758c9ff72f48e", size = 11342470, upload-time = "2026-04-28T10:45:26.455Z" }, - { url = "https://files.pythonhosted.org/packages/ab/62/7fb948aace38d2f6329261bb33c035a8484549c74f1db28649c7a4c6fed9/ty-0.0.33-py3-none-win_arm64.whl", hash = "sha256:0d44f99ba1b441e55e2aa301b2ac0a21112784931b46a5f66f4ea9efe5620d97", size = 10742673, upload-time = "2026-04-28T10:45:35.555Z" }, +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" }, + { url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" }, + { url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" }, + { url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" }, + { url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" }, ] [[package]]