diff --git a/pyproject.toml b/pyproject.toml index 58016d48..e1769eca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -171,13 +171,48 @@ unused-ignore-comment = "error" [[tool.ty.overrides]] include = [ "src/_pytask/_version.py", - "src/_pytask/click.py", "tests/test_dag_command.py", ] [tool.ty.overrides.rules] unused-ignore-comment = "ignore" +[[tool.ty.overrides]] +include = ["src/_pytask/click.py"] + +[tool.ty.overrides.rules] +unresolved-attribute = "ignore" +unused-ignore-comment = "ignore" + +[[tool.ty.overrides]] +include = ["src/_pytask/debugging.py"] + +[tool.ty.overrides.rules] +unsupported-base = "ignore" + +[[tool.ty.overrides]] +include = ["src/_pytask/hookspecs.py"] + +[tool.ty.overrides.rules] +empty-body = "ignore" + +[[tool.ty.overrides]] +include = ["src/_pytask/warnings_utils.py"] + +[tool.ty.overrides.rules] +unresolved-attribute = "ignore" + +[[tool.ty.overrides]] +include = ["tests/**/*.py", "tests/**/*.ipynb"] + +[tool.ty.overrides.rules] +call-non-callable = "ignore" +invalid-argument-type = "ignore" +invalid-return-type = "ignore" +unresolved-attribute = "ignore" +unresolved-reference = "ignore" +unsupported-operator = "ignore" + [tool.ty.src] include = ["src", "tests"] exclude = ["src/_pytask/_hashlib.py"] diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index 8f0866ad..33f00143 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -244,7 +244,7 @@ def __exit__( @property def buffer(self) -> BinaryIO: # The str/bytes doesn't actually matter in this type, so OK to fake. - return self # type: ignore[return-value] + return cast("BinaryIO", self) # Capture classes. @@ -553,7 +553,7 @@ def snap(self) -> bytes: res = self.tmpfile.buffer.read() self.tmpfile.seek(0) self.tmpfile.truncate() - return res # type: ignore[return-value] + return cast("bytes", res) def writeorg(self, data: bytes) -> None: """Write to original file descriptor.""" @@ -656,8 +656,10 @@ def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]: """Pop current snapshot out/err capture and flush to orig streams.""" out, err = self.readouterr() if out: + assert self.out is not None self.out.writeorg(out) # type: ignore[union-attr] if err: + assert self.err is not None self.err.writeorg(err) # type: ignore[union-attr] return out, err @@ -678,7 +680,8 @@ def resume_capturing(self) -> None: if self.err: self.err.resume() if self._in_suspended: - self.in_.resume() # type: ignore[union-attr] + assert self.in_ is not None + self.in_.resume() self._in_suspended = False def stop_capturing(self) -> None: @@ -699,10 +702,9 @@ def is_started(self) -> bool: return self._state == "started" def readouterr(self) -> CaptureResult[AnyStr]: - out = self.out.snap() if self.out else "" - err = self.err.snap() if self.err else "" - # Will be fixed by pytest. This type error is real, need to fix. - return CaptureResult(out, err) # type: ignore[arg-type] + out = self.out.snap() if self.out else cast("AnyStr", "") + err = self.err.snap() if self.err else cast("AnyStr", "") + return CaptureResult(out, err) def _get_multicapture(method: CaptureMethod) -> MultiCapture[str]: diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index a5eacdde..ecbfeab0 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -192,7 +192,8 @@ def _collect_not_collected_tasks(session: Session) -> None: for path in list(COLLECTED_TASKS): tasks = COLLECTED_TASKS.pop(path) for task in tasks: - name = task.pytask_meta.name # type: ignore[attr-defined] + name = task.pytask_meta.name + assert name is not None node: PTask if path: node = Task(base_name=name, path=path, function=task) diff --git a/src/_pytask/config_utils.py b/src/_pytask/config_utils.py index 22bfbe15..0cf3c8d4 100644 --- a/src/_pytask/config_utils.py +++ b/src/_pytask/config_utils.py @@ -43,7 +43,9 @@ def set_defaults_from_config( # command-line options during parsing. Here, we add their defaults to the # configuration. command_option_names = [option.name for option in context.command.params] - commands = context.parent.command.commands # type: ignore[union-attr] + assert context.parent is not None + assert isinstance(context.parent.command, click.Group) + commands = context.parent.command.commands all_defaults_from_cli = { option.name: option.default for name, command in commands.items() diff --git a/src/_pytask/console.py b/src/_pytask/console.py index 3c4b91e7..aa2ced94 100644 --- a/src/_pytask/console.py +++ b/src/_pytask/console.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from typing import Literal from typing import cast from rich.console import Console @@ -32,7 +33,6 @@ from collections.abc import Callable from collections.abc import Iterable from collections.abc import Sequence - from enum import Enum from _pytask.node_protocols import PTask from _pytask.outcomes import CollectionOutcome @@ -119,7 +119,10 @@ def render_to_string( except (AttributeError, IndexError, TypeError): theme = None render_console = Console( - color_system=console.color_system, # type: ignore[invalid-argument-type] + color_system=cast( + "Literal['auto', 'standard', '256', 'truecolor', 'windows'] | None", + console.color_system, + ), force_terminal=True, width=console.width, no_color=False, @@ -282,7 +285,7 @@ def unify_styles(*styles: str | Style) -> Style: def create_summary_panel( - counts: dict[Enum, int], + counts: dict[CollectionOutcome | TaskOutcome, int], outcome_enum: type[CollectionOutcome | TaskOutcome], description_total: str, ) -> Panel: @@ -302,14 +305,14 @@ def create_summary_panel( grid.add_row( Padding(str(value), pad=_HORIZONTAL_PADDING), Padding( - outcome.description, # type: ignore[attr-defined] + outcome.description, pad=_HORIZONTAL_PADDING, ), Padding( percentage, pad=_HORIZONTAL_PADDING, ), - style=outcome.style_textonly, # type: ignore[attr-defined] + style=outcome.style_textonly, ) return Panel( diff --git a/src/_pytask/data_catalog.py b/src/_pytask/data_catalog.py index 29ebd21a..9ad4cde0 100644 --- a/src/_pytask/data_catalog.py +++ b/src/_pytask/data_catalog.py @@ -125,7 +125,8 @@ def add(self, name: str, node: PNode | PProvisionalNode | Any = None) -> None: ) else: self._entries[name] = self.default_node(name=name) - self.path.joinpath(f"{filename}-node.pkl").write_bytes( # type: ignore[union-attr] + assert self.path is not None + self.path.joinpath(f"{filename}-node.pkl").write_bytes( pickle.dumps(self._entries[name]) ) elif isinstance(node, (PNode, PProvisionalNode)): diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index f841bdac..267174f2 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -76,7 +76,8 @@ def _parse_pdbcls(value: str | None) -> tuple[str, str] | None: "Must be like 'IPython.terminal.debugger:TerminalPdb'" ) raise ValueError(msg) - return tuple(split) # type: ignore[return-value] + modname, classname = split + return modname, classname def _pdbcls_callback( @@ -383,7 +384,8 @@ def wrapper(*args: Any, **kwargs: Any) -> None: console.rule("Traceback", characters=">", style="default") console.print(Traceback(exc_info)) - post_mortem(exc_info[2]) # type: ignore[arg-type] + assert exc_info[2] is not None + post_mortem(exc_info[2]) live_manager.resume() capman.resume() diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 923049e0..6cf2f2e1 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -7,6 +7,7 @@ import time from typing import TYPE_CHECKING from typing import Any +from typing import cast from rich.text import Text @@ -220,7 +221,7 @@ def pytask_execute_task_setup(session: Session, task: PTask) -> None: # noqa: C # Cast reason since get_node_change_info can return "unchanged" # but we only call this when has_changed is True - reason_typed: ReasonType = reason # type: ignore[assignment] + reason_typed = cast("ReasonType", reason) change_reasons.append( create_change_reason( node=node, diff --git a/src/_pytask/hookspecs.py b/src/_pytask/hookspecs.py index 1830f41b..fcecbf54 100644 --- a/src/_pytask/hookspecs.py +++ b/src/_pytask/hookspecs.py @@ -180,7 +180,7 @@ def pytask_collect_task_setup( @hookspec(firstresult=True) def pytask_collect_task( session: Session, path: Path | None, name: str, obj: Any -) -> PTask: # ty: ignore[empty-body] +) -> PTask: """Collect a single task.""" diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py index 6898b930..b6a2c70e 100644 --- a/src/_pytask/mark/structures.py +++ b/src/_pytask/mark/structures.py @@ -8,6 +8,8 @@ from _pytask.mark_utils import get_all_marks from _pytask.models import CollectionMetadata from _pytask.typing import TaskFunction +from _pytask.typing import attach_task_metadata +from _pytask.typing import is_task_decorator_target from _pytask.typing import is_task_function if TYPE_CHECKING: @@ -174,9 +176,10 @@ def store_mark(obj: Callable[..., Any], mark: Mark) -> None: if isinstance(obj, TaskFunction): obj.pytask_meta.markers = [*get_unpacked_marks(obj), mark] else: - obj.pytask_meta = CollectionMetadata( # type: ignore[attr-defined] - markers=[mark] - ) + if not is_task_decorator_target(obj): + msg = "Marks can only be stored on user-defined callables." + raise TypeError(msg) + attach_task_metadata(obj, CollectionMetadata(markers=[mark])) _DEPRECATION_DECORATOR = """'@pytask.mark.{}' was removed in pytask v0.5.0. To upgrade \ diff --git a/src/_pytask/mark_utils.py b/src/_pytask/mark_utils.py index f4f8b188..19bbc118 100644 --- a/src/_pytask/mark_utils.py +++ b/src/_pytask/mark_utils.py @@ -12,6 +12,8 @@ from _pytask.models import CollectionMetadata from _pytask.node_protocols import PTask from _pytask.typing import TaskFunction +from _pytask.typing import attach_task_metadata +from _pytask.typing import is_task_decorator_target if TYPE_CHECKING: from _pytask.mark import Mark @@ -32,7 +34,10 @@ def set_marks(obj_or_task: Any | PTask, marks: list[Mark]) -> Any | PTask: elif isinstance(obj_or_task, TaskFunction): obj_or_task.pytask_meta.markers = marks else: - obj_or_task.pytask_meta = CollectionMetadata(markers=marks) + if not is_task_decorator_target(obj_or_task): + msg = "Marks can only be stored on tasks or user-defined callables." + raise TypeError(msg) + attach_task_metadata(obj_or_task, CollectionMetadata(markers=marks)) return obj_or_task diff --git a/src/_pytask/nodes.py b/src/_pytask/nodes.py index af728ce8..5d128d4c 100644 --- a/src/_pytask/nodes.py +++ b/src/_pytask/nodes.py @@ -400,13 +400,15 @@ def signature(self) -> str: def load(self, is_product: bool = False) -> Path: """Inject a path into the task when loaded as a product.""" if is_product: - return self.root_dir # type: ignore[return-value] + assert self.root_dir is not None + return self.root_dir msg = "'DirectoryNode' cannot be loaded as a dependency" # pragma: no cover raise NotImplementedError(msg) # pragma: no cover def collect(self) -> list[Path]: """Collect paths defined by the pattern.""" - return list(self.root_dir.glob(self.pattern)) # type: ignore[union-attr] + assert self.root_dir is not None + return list(self.root_dir.glob(self.pattern)) def get_state_of_path(path: NodePath) -> str | None: diff --git a/src/_pytask/outcomes.py b/src/_pytask/outcomes.py index 3dd4a2e1..222e713a 100644 --- a/src/_pytask/outcomes.py +++ b/src/_pytask/outcomes.py @@ -172,7 +172,7 @@ def style_textonly(self) -> str: def count_outcomes( reports: Sequence[CollectionReport | ExecutionReport], outcome_enum: type[CollectionOutcome | TaskOutcome], -) -> dict[Enum, int]: +) -> dict[CollectionOutcome | TaskOutcome, int]: """Count how often an outcome occurred. Examples diff --git a/src/_pytask/reports.py b/src/_pytask/reports.py index bf88f33e..8f4c06c5 100644 --- a/src/_pytask/reports.py +++ b/src/_pytask/reports.py @@ -48,7 +48,8 @@ def __rich_console__( self, console: Console, console_options: ConsoleOptions ) -> RenderResult: header = "Error" if self.node is None else f"Could not collect {self.node.name}" - traceback = Traceback(self.exc_info) # type: ignore[arg-type] + assert self.exc_info is not None + traceback = Traceback(self.exc_info) yield Rule( Text(header, style=CollectionOutcome.FAIL.style), style=CollectionOutcome.FAIL.style, @@ -107,7 +108,8 @@ def __rich_console__( task=self.task, editor_url_scheme=self.editor_url_scheme ) text = Text.assemble("Task ", task_name, " failed", style="failed") - traceback = Traceback(self.exc_info) # type: ignore[arg-type] + assert self.exc_info is not None + traceback = Traceback(self.exc_info) yield Rule(text, style=self.outcome.style) yield "" diff --git a/src/_pytask/task.py b/src/_pytask/task.py index 597d3267..4ab7ae02 100644 --- a/src/_pytask/task.py +++ b/src/_pytask/task.py @@ -12,11 +12,11 @@ from _pytask.task_utils import parse_collected_tasks_with_task_marker if TYPE_CHECKING: - from collections.abc import Callable from pathlib import Path from _pytask.reports import CollectionReport from _pytask.session import Session + from _pytask.typing import TaskFunction @hookimpl @@ -59,7 +59,7 @@ def pytask_collect_file( def _raise_error_when_task_functions_are_duplicated( - tasks: list[Callable[..., Any]], + tasks: list[TaskFunction], ) -> None: """Raise error when task functions are duplicated. diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index d44818ca..34b2d1e1 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -25,7 +25,8 @@ from _pytask.shared import find_duplicates from _pytask.shared import unwrap_task_function from _pytask.typing import TaskFunction -from _pytask.typing import is_task_function +from _pytask.typing import attach_task_metadata +from _pytask.typing import is_task_decorator_target as is_task_decorator_target_runtime if TYPE_CHECKING: from collections.abc import Callable @@ -41,7 +42,7 @@ def _is_task_decorator_target(obj: object) -> TypeGuard[Callable[..., Any]]: """Narrow objects accepted by bare ``@task`` usage to named callables.""" - return is_task_function(obj) + return is_task_decorator_target_runtime(obj) __all__ = [ @@ -52,7 +53,7 @@ def _is_task_decorator_target(obj: object) -> TypeGuard[Callable[..., Any]]: ] -COLLECTED_TASKS: dict[Path | None, list[Callable[..., Any]]] = defaultdict(list) +COLLECTED_TASKS: dict[Path | None, list[TaskFunction]] = defaultdict(list) """A container for collecting tasks. Tasks marked by the [`@task`][pytask.task] decorator can be generated in a loop @@ -186,6 +187,9 @@ def wrapper(func: T) -> TaskDecorated[T]: "the builtin function in a function or lambda expression." ) raise NotImplementedError(msg) + if not is_task_decorator_target_runtime(unwrapped): + msg = "Task functions must be user-defined callables or functools.partial." + raise TypeError(msg) path = get_file(unwrapped) @@ -203,18 +207,22 @@ def wrapper(func: T) -> TaskDecorated[T]: unwrapped.pytask_meta.produces = produces unwrapped.pytask_meta.annotation_locals = caller_locals else: - unwrapped.pytask_meta = CollectionMetadata( # type: ignore[attr-defined] - after=parsed_after, - annotation_locals=caller_locals, - is_generator=is_generator, - id_=id, - kwargs=parsed_kwargs, - markers=[Mark("task", (), {})], - name=parsed_name, - produces=produces, + attach_task_metadata( + unwrapped, + CollectionMetadata( + after=parsed_after, + annotation_locals=caller_locals, + is_generator=is_generator, + id_=id, + kwargs=parsed_kwargs, + markers=[Mark("task", (), {})], + name=parsed_name, + produces=produces, + ), ) + assert isinstance(unwrapped, TaskFunction) - if coiled_kwargs and isinstance(unwrapped, TaskFunction): + if coiled_kwargs: unwrapped.pytask_meta.attributes["coiled_kwargs"] = coiled_kwargs # Store it in the global variable ``COLLECTED_TASKS`` to avoid garbage @@ -269,8 +277,8 @@ def _parse_after( def parse_collected_tasks_with_task_marker( - tasks: list[Callable[..., Any]], -) -> dict[str, Callable[..., Any]]: + tasks: list[TaskFunction], +) -> dict[str, TaskFunction]: """Parse collected tasks with a task marker.""" parsed_tasks = _parse_tasks_with_preliminary_names(tasks) all_names = {i[0] for i in parsed_tasks} @@ -289,8 +297,8 @@ def parse_collected_tasks_with_task_marker( def _parse_tasks_with_preliminary_names( - tasks: list[Callable[..., Any]], -) -> list[tuple[str, Callable[..., Any]]]: + tasks: list[TaskFunction], +) -> list[tuple[str, TaskFunction]]: """Parse tasks and generate preliminary names for tasks. The names are preliminary since they can be duplicated and need to be extended to @@ -304,9 +312,9 @@ def _parse_tasks_with_preliminary_names( return parsed_tasks -def _parse_task(task: Callable[..., Any]) -> tuple[str, Callable[..., Any]]: +def _parse_task(task: TaskFunction) -> tuple[str, TaskFunction]: """Parse a single task.""" - meta = task.pytask_meta # type: ignore[attr-defined] + meta = task.pytask_meta task_name = getattr(task, "__name__", "_") if meta.name is None and task_name == "_": @@ -354,24 +362,22 @@ def parse_keyword_arguments_from_signature_defaults( def _generate_ids_for_tasks( - tasks: list[tuple[str, Callable[..., Any]]], -) -> dict[str, Callable[..., Any]]: + tasks: list[tuple[str, TaskFunction]], +) -> dict[str, TaskFunction]: """Generate unique ids for parametrized tasks.""" parameters = inspect.signature(tasks[0][1]).parameters - out = {} + out: dict[str, TaskFunction] = {} for i, (name, task) in enumerate(tasks): - if task.pytask_meta.id_ is not None: # type: ignore[attr-defined] - id_ = f"{name}[{task.pytask_meta.id_}]" # type: ignore[attr-defined] + if task.pytask_meta.id_ is not None: + id_ = f"{name}[{task.pytask_meta.id_}]" elif not parameters: id_ = f"{name}[{i}]" else: stringified_args = [ _arg_value_to_id_component( arg_name=parameter, - arg_value=task.pytask_meta.kwargs.get( # type: ignore[attr-defined] - parameter - ), + arg_value=task.pytask_meta.kwargs.get(parameter), i=i, id_func=None, ) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index dbe7174f..7baf17c9 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -81,7 +81,9 @@ def remove_traceback_from_exc_info( exc_info: OptionalExceptionInfo, ) -> OptionalExceptionInfo: """Remove traceback from exception.""" - return (exc_info[0], exc_info[1], None) # type: ignore[return-value] + if exc_info[0] is None or exc_info[1] is None: + return (None, None, None) + return (exc_info[0], exc_info[1], None) def _remove_internal_traceback_frames_from_exc_info( diff --git a/src/_pytask/typing.py b/src/_pytask/typing.py index ef479c8b..fb0b25dc 100644 --- a/src/_pytask/typing.py +++ b/src/_pytask/typing.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import Path +from types import BuiltinFunctionType from typing import TYPE_CHECKING from typing import Any from typing import Final @@ -24,7 +25,10 @@ "NodePath", "Product", "ProductType", + "TaskDecoratorTarget", "TaskFunction", + "attach_task_metadata", + "is_task_decorator_target", "is_task_function", "no_default", ] @@ -43,9 +47,20 @@ class TaskFunction(Protocol): We don't require __name__ to support functools.partial. """ + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + pytask_meta: CollectionMetadata +@runtime_checkable +class TaskDecoratorTarget(Protocol): + """Protocol for callables that can store pytask metadata on ``__dict__``.""" + + __dict__: dict[str, Any] + + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + + @dataclass(frozen=True) class ProductType: """A class to mark products.""" @@ -62,6 +77,23 @@ def is_task_function(obj: Any) -> bool: ) +def is_task_decorator_target(obj: Any) -> bool: + """Check if an object can store pytask metadata on itself.""" + return ( + is_task_function(obj) + and not isinstance(obj, BuiltinFunctionType) + and isinstance(obj, TaskDecoratorTarget) + ) + + +def attach_task_metadata( + obj: TaskDecoratorTarget, metadata: CollectionMetadata +) -> None: + """Attach pytask metadata to a decorator target.""" + attribute_name = "pytask_meta" + setattr(obj, attribute_name, metadata) + + def is_task_generator(task: PTask) -> bool: """Check if a task is a generator.""" return task.attributes.get("is_generator", False) diff --git a/uv.lock b/uv.lock index a2c4c750..23726f09 100644 --- a/uv.lock +++ b/uv.lock @@ -3913,26 +3913,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.24" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/96/652a425030f95dc2c9548d9019e52502e17079e1daeefbc4036f1c0905b4/ty-0.0.24.tar.gz", hash = "sha256:9fe42f6b98207bdaef51f71487d6d087f2cb02555ee3939884d779b2b3cc8bfc", size = 5354286, upload-time = "2026-03-19T16:55:57.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/e5/34457ee11708e734ba81ad65723af83030e484f961e281d57d1eecf08951/ty-0.0.24-py3-none-linux_armv6l.whl", hash = "sha256:1ab4f1f61334d533a3fdf5d9772b51b1300ac5da4f3cdb0be9657a3ccb2ce3e7", size = 10394877, upload-time = "2026-03-19T16:55:54.246Z" }, - { url = "https://files.pythonhosted.org/packages/44/81/bc9a1b1a87f43db15ab64ad781a4f999734ec3b470ad042624fa875b20e6/ty-0.0.24-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:facbf2c4aaa6985229e08f8f9bf152215eb078212f22b5c2411f35386688ab42", size = 10211109, upload-time = "2026-03-19T16:55:28.554Z" }, - { url = "https://files.pythonhosted.org/packages/e4/63/cfc805adeaa61d63ba3ea71127efa7d97c40ba36d97ee7bd957341d05107/ty-0.0.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b6d2a3b6d4470c483552a31e9b368c86f154dcc964bccb5406159dc9cd362246", size = 9694769, upload-time = "2026-03-19T16:55:34.309Z" }, - { url = "https://files.pythonhosted.org/packages/33/09/edc220726b6ec44a58900401f6b27140997ef15026b791e26b69a6e69eb5/ty-0.0.24-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c94c25d0500939fd5f8f16ce41cbed5b20528702c1d649bf80300253813f0a2", size = 10176287, upload-time = "2026-03-19T16:55:37.17Z" }, - { url = "https://files.pythonhosted.org/packages/f8/bf/cbe2227be711e65017655d8ee4d050f4c92b113fb4dc4c3bd6a19d3a86d8/ty-0.0.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:89cbe7bc7df0fab02dbd8cda79b737df83f1ef7fb573b08c0ee043dc68cffb08", size = 10214832, upload-time = "2026-03-19T16:56:08.518Z" }, - { url = "https://files.pythonhosted.org/packages/af/1d/d15803ee47e9143d10e10bd81ccc14761d08758082bda402950685f0ddfe/ty-0.0.24-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2c5d269bcc9b764850c99f457b5018a79b3ef40ecfbc03344e65effd6cf743", size = 10709892, upload-time = "2026-03-19T16:56:05.727Z" }, - { url = "https://files.pythonhosted.org/packages/36/12/6db0d86c477147f67b9052de209421d76c3e855197b000c25fcbbe86b3a2/ty-0.0.24-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba44512db5b97c3bbd59d93e11296e8548d0c9a3bdd1280de36d7ff22d351896", size = 11280872, upload-time = "2026-03-19T16:56:02.899Z" }, - { url = "https://files.pythonhosted.org/packages/1b/fc/155fe83a97c06d33ccc9e0f428258b32df2e08a428300c715d34757f0111/ty-0.0.24-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a52b7f589c3205512a9c50ba5b2b1e8c0698b72e51b8b9285c90420c06f1cae8", size = 11060520, upload-time = "2026-03-19T16:55:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f1/32c05a1c4c3c2a95c5b7361dee03a9bf1231d4ad096b161c838b45bce5a0/ty-0.0.24-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7981df5c709c054da4ac5d7c93f8feb8f45e69e829e4461df4d5f0988fe67d04", size = 10791455, upload-time = "2026-03-19T16:55:25.728Z" }, - { url = "https://files.pythonhosted.org/packages/17/2c/53c1ea6bedfa4d4ab64d4de262d8f5e405ecbffefd364459c628c0310d33/ty-0.0.24-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2860151ad95a00d0f0280b8fef79900d08dcd63276b57e6e5774f2c055979c5", size = 10156708, upload-time = "2026-03-19T16:55:45.563Z" }, - { url = "https://files.pythonhosted.org/packages/45/39/7d2919cf194707169474d80720a5f3d793e983416f25e7ffcf80504c9df2/ty-0.0.24-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5674a1146d927ab77ff198a88e0c4505134ced342a0e7d1beb4a076a728b7496", size = 10236263, upload-time = "2026-03-19T16:55:31.474Z" }, - { url = "https://files.pythonhosted.org/packages/cf/7f/48eac722f2fd12a5b7aae0effdcb75c46053f94b783d989e3ef0d7380082/ty-0.0.24-py3-none-musllinux_1_2_i686.whl", hash = "sha256:438ecbf1608a9b16dd84502f3f1b23ef2ef32bbd0ab3e0ca5a82f0e0d1cd41ea", size = 10402559, upload-time = "2026-03-19T16:55:39.602Z" }, - { url = "https://files.pythonhosted.org/packages/75/e0/8cf868b9749ce1e5166462759545964e95b02353243594062b927d8bff2a/ty-0.0.24-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ddeed3098dd92a83964e7aa7b41e509ba3530eb539fc4cd8322ff64a09daf1f5", size = 10893684, upload-time = "2026-03-19T16:55:51.439Z" }, - { url = "https://files.pythonhosted.org/packages/17/9f/f54bf3be01d2c2ed731d10a5afa3324dc66f987a6ae0a4a6cbfa2323d080/ty-0.0.24-py3-none-win32.whl", hash = "sha256:83013fb3a4764a8f8bcc6ca11ff8bdfd8c5f719fc249241cb2b8916e80778eb1", size = 9781542, upload-time = "2026-03-19T16:56:11.588Z" }, - { url = "https://files.pythonhosted.org/packages/fb/49/c004c5cc258b10b3a145666e9a9c28ae7678bc958c8926e8078d5d769081/ty-0.0.24-py3-none-win_amd64.whl", hash = "sha256:748a60eb6912d1cf27aaab105ffadb6f4d2e458a3fcadfbd3cf26db0d8062eeb", size = 10764801, upload-time = "2026-03-19T16:55:42.752Z" }, - { url = "https://files.pythonhosted.org/packages/e2/59/006a074e185bfccf5e4c026015245ab4fcd2362b13a8d24cf37a277909a9/ty-0.0.24-py3-none-win_arm64.whl", hash = "sha256:280a3d31e86d0721947238f17030c33f0911cae851d108ea9f4e3ab12a5ed01f", size = 10194093, upload-time = "2026-03-19T16:55:48.303Z" }, +version = "0.0.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/21/3ee32f163038ac2663c7bea47a07d06bf4cc7c09d95b96db194bda1b70cb/ty-0.0.30.tar.gz", hash = "sha256:c982207640e7d75331b81031ebfb884ab858ed26ab16d7c086ac4942e2771846", size = 5518350, upload-time = "2026-04-14T13:53:35.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/24/7aa94d02a9257ed96e64e4e99b527f28390febd8424107b4f8a70763ace9/ty-0.0.30-py3-none-linux_armv6l.whl", hash = "sha256:1be31a24a2a177571c3276854bf01b2b1a77dba6e754507089c25bb1825ce5f6", size = 10801835, upload-time = "2026-04-14T13:53:21.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/97/2410ebc85cfcdf3bbd0e5958c6cd0b88085b1a184374ecfa755f84d6c8b2/ty-0.0.30-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:019f1d0d5d5265a1e634a51fd49374df43dafae14de98c2a0d349beb8233550b", size = 10582386, upload-time = "2026-04-14T13:53:07.472Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d2/a2649eb6841ebf946ac827e778b7e78b5ef63c3758bf2b9da13d927a53da/ty-0.0.30-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe3012af4d0714e7353fd3cf6d2d02d5b0f0fe6f1cb8beb2366ed9f621c2c349", size = 10031621, upload-time = "2026-04-14T13:53:01.523Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8e/40a66ccd5d5d51adf0469b9fbe4f1f79f928a880b34b8a6c7c934e8a883a/ty-0.0.30-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1e90b4ebf6310c7734344739e0950f4cede5a33b1e51a12a0c0fc8a975866ed", size = 10537511, upload-time = "2026-04-14T13:53:04.538Z" }, + { url = "https://files.pythonhosted.org/packages/25/31/5dea2987601ef1c8c58b04f2173971e7fe51f7902ab93a66d09e0f12115a/ty-0.0.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd83a0d82cbc32c2ae521e7fa101fb5fe5b566adb1364996582535700572a9ec", size = 10603406, upload-time = "2026-04-14T13:53:47.564Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a4/5a7585b6b219a2edc00255af0b16a8475f88fe43c5cdbe499daecb67f100/ty-0.0.30-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:672a29271c13247096d0b2766e69cb35b1583882dd6e7b24065927e2491ffe6d", size = 11109133, upload-time = "2026-04-14T13:53:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/b9/83/b402dc4bd99b6f3eb0bce04e557889a164e099976a7fc71a6b07c923241b/ty-0.0.30-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91ff236adbb90281c05f7e160664820be50f42d3a9d8f1d0a648f006864114fa", size = 11663362, upload-time = "2026-04-14T13:53:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/65/1b/8157f03acc15421083c194b11a61a78d10e3dfa7e4a0177809fc9acc3881/ty-0.0.30-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ec99bd5d5430c52fb64038483deb070f12c7ae78ffd6d6841d31719daedf1d7", size = 11304786, upload-time = "2026-04-14T13:53:30.076Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c3/f89a9a42b47da108ed758ae9d065d10bf2acc2ea88e3d200b95511096b7b/ty-0.0.30-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4b328ee332ec6276afc863ea7cf6d8167d9dd8d9f3d1c2e738ef39932511ac4", size = 11173426, upload-time = "2026-04-14T13:53:10.262Z" }, + { url = "https://files.pythonhosted.org/packages/81/37/fa38ee0259dc49579e1871b23ab1ff27331a78460566cdc13045a237595d/ty-0.0.30-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fd0d664d6530890a8e872accd96895410773e7a4c6d20c244fb7a5f541ff359b", size = 10517157, upload-time = "2026-04-14T13:53:15.739Z" }, + { url = "https://files.pythonhosted.org/packages/2e/79/28032481141eb6ce3274f62b9ff9b1d73d59df6b28080c8fe3c6bdef700e/ty-0.0.30-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:314004166a7a5e39e169c7da0b9e78f3315382f53db8698fd98346cee3bb0784", size = 10613222, upload-time = "2026-04-14T13:53:13.269Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/989fca4c74095defd7d3ba5afc68a5aa4e2ca428fedfca5df526701c730b/ty-0.0.30-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d969ebf9d8b08e93e638c56e6fb5a8dacd2a24f43e3519479d245ddde69f968e", size = 10789624, upload-time = "2026-04-14T13:53:42.156Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/3e74aba392ba2eeae5d86568ee282d9d6b2b6642445e3d9837c88d73c282/ty-0.0.30-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:66922c8c4381a016f90ec4b811748e7bb12da892f4c273640710da721caea7fb", size = 11260273, upload-time = "2026-04-14T13:53:44.974Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/e94a0e5e5a1850a2ba61c5efcfa594cfc2d23c026bf431cce33003d036a0/ty-0.0.30-py3-none-win32.whl", hash = "sha256:b7b2ecf80c872d7d9928b372e99233bdda7cabe639edd06b6232c3161a7dfa40", size = 10145096, upload-time = "2026-04-14T13:53:39.335Z" }, + { url = "https://files.pythonhosted.org/packages/50/d3/09c8df72ad37f7f4d9d79fe04a08bfa649d9f141d137e624fc23c7c3d7fe/ty-0.0.30-py3-none-win_amd64.whl", hash = "sha256:f29834e3d96c447f2adcf9eeb55b3f92005c91f52597c4c46d844188ec67ec72", size = 11156009, upload-time = "2026-04-14T13:53:32.847Z" }, + { url = "https://files.pythonhosted.org/packages/e6/17/a5c049c36e2fef9c593a1862f275af963b66045378f10b6908c6f10f6f4a/ty-0.0.30-py3-none-win_arm64.whl", hash = "sha256:d9be1d258dab615b447d20fa58633f0ae163af01bfa781a50457defec20642fd", size = 10552887, upload-time = "2026-04-14T13:53:27.455Z" }, ] [[package]]