Compare commits

...

9 Commits

Author SHA1 Message Date
6f8b896bba Update LICENSE for 2026 2026-04-01 21:33:37 +02:00
10f81ee53c Implement help 2026-04-01 21:32:06 +02:00
d5e815dc32 Default parse_args to sys.argv 2026-04-01 21:31:33 +02:00
fc8a192a07 Implement choices help for Enum args 2026-04-01 21:17:40 +02:00
02ee0dab3d Do a lot of changes
- Add ty to dev deps;
- Add some tests covering previously uncovered cases;
- Fix some bugs;
- Introduce argfield for nicer argparse interface for things that can't
  be expressed via the type system;
- Fix line length.
2026-04-01 20:56:44 +02:00
96af0718ee Migrate build system to uv 2026-03-31 20:47:17 +02:00
bd4389e806 Remove black and bump ruff 2026-03-31 20:31:36 +02:00
05a2506b1e Configure linting and setup deps via uv 2025-05-14 21:41:08 +02:00
81842e6d19 Migrate to pyproject.toml for build 2025-05-14 21:33:34 +02:00
8 changed files with 349 additions and 178 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
__pycache__/ __pycache__/
*.egg-info/

View File

@@ -1,4 +1,4 @@
Copyright 2021 Pavel Lutskov Copyright 2021-2026 Pavel Lutskov
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@@ -1,111 +0,0 @@
import typing
from argparse import ArgumentParser
from dataclasses import dataclass, MISSING
def make_gnu_option(name):
return f'--{name.replace("_", "-")}'
def decide_default(field_):
arg_cfg = {}
if field_.default != MISSING:
arg_cfg["default"] = field_.default
elif field_.default_factory != MISSING:
arg_cfg["default"] = field_.default_factory()
else:
arg_cfg["required"] = True
return arg_cfg
def get_choices(field_):
arg_cfg = {}
try:
arg_cfg["choices"] = field_.metadata["choices"]
except KeyError:
pass
return arg_cfg
def compute_arg_names(name, field_):
names = [make_gnu_option(name)] # long option
try:
names.append(f'-{field_.metadata["shortopt"]}')
except KeyError:
pass
return names
def _prepare_bool(ap: ArgumentParser, name, field_):
arg_cfg = decide_default(field_)
required = "required" in arg_cfg
bool_parser = ap.add_mutually_exclusive_group(required=required)
bool_parser.add_argument(
make_gnu_option(name), action="store_true", dest=name
)
bool_parser.add_argument(
make_gnu_option(f"no_{name}"), action="store_false", dest=name
)
if not required:
ap.set_defaults(**{name: arg_cfg["default"]})
def _prepare_list_cfg(name, field_):
arg_cfg = {
**decide_default(field_),
**get_choices(field_),
}
subtype = typing.get_args(field_.type)
if not subtype:
arg_cfg["type"] = str
else:
arg_cfg["type"] = subtype[0]
if field_.metadata.get("allow_empty", False):
arg_cfg["nargs"] = "*"
else:
arg_cfg["nargs"] = "+"
return arg_cfg
def _prepare_trivial_cfg(name, field_):
arg_cfg = {
**decide_default(field_),
**get_choices(field_),
}
arg_cfg["type"] = field_.type
return arg_cfg
def _prepare_list(ap: ArgumentParser, name, field_):
arg_cfg = _prepare_list_cfg(name, field_)
arg_names = compute_arg_names(name, field_)
ap.add_argument(*arg_names, **arg_cfg)
def _prepare_trivial(ap: ArgumentParser, name, field_):
arg_cfg = _prepare_trivial_cfg(name, field_)
arg_names = compute_arg_names(name, field_)
ap.add_argument(*arg_names, **arg_cfg)
def prepare_field(ap, name, field_):
if field_.type is bool:
_prepare_bool(ap, name, field_)
elif field_.type is list or typing.get_origin(field_.type) is list:
_prepare_list(ap, name, field_)
else:
_prepare_trivial(ap, name, field_)
def argclass(cls):
@classmethod
def parse_args(cls, argv):
ap = ArgumentParser()
for name, field_ in cls.__dataclass_fields__.items():
prepare_field(ap, name, field_)
return cls(**vars(ap.parse_args(argv)))
cls = dataclass(cls)
cls.parse_args = parse_args
return cls

View File

@@ -1,10 +1,38 @@
[project]
name = "argclass"
description = "A simple dataclass-based interface to Python's argparse"
license = "MIT"
license-files = ["LICENSE"]
authors = [
{name = "Pavel Lutskov", email = "pavel.lutskov@gmail.com"},
]
requires-python = ">=3.10"
classifiers = [
"Private :: Do Not Upload",
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
version = "0.1.0.dev1"
[project.urls]
Repository = "https://git.ltskv.de/pavel/argclass"
[build-system] [build-system]
requires = [ requires = [
"setuptools>=42", "uv_build>=0.10.6,<0.11",
"wheel"
] ]
build-backend = "setuptools.build_meta" build-backend = "uv_build"
[tool.black] [tool.ruff]
line-length = 79 line-length = 79
target-version = ["py38"]
[tool.ruff.lint]
select = ["F", "B", "I"]
fixable = ["I"]
[dependency-groups]
dev = [
"ruff>=0.15",
"ty>=0.0.27",
]

View File

@@ -1,15 +0,0 @@
[metadata]
name = argclass
version = 0.1.0
author = Pavel Lutskov
author_email = pavel.lutskov@gmail.com
description = A simple dataclass-based interface to Python's argparse
url = https://git.deguo.duckdns.org/pavel/argclass
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
py_modules = argclass
python_requires = >=3.8

175
src/argclass/__init__.py Normal file
View File

@@ -0,0 +1,175 @@
# Copyright 2021-2026 Pavel Lutskov
import typing
from argparse import ArgumentParser
from dataclasses import MISSING, Field, dataclass, field
from enum import Enum
SHORTOPT = "__argclass__shortopt"
CHOICES = "__argclass__choices"
ALLOW_EMPTY = "__argclass__allow_empty"
HELP = "__argclass__help"
class ArgclassError(Exception):
pass
def _make_gnu_option(name: str):
assert name.isidentifier()
return f"--{name.replace('_', '-')}"
def _decide_default(field_: Field):
defaults_cfg = {}
if field_.default != MISSING:
defaults_cfg["default"] = field_.default
elif field_.default_factory != MISSING:
defaults_cfg["default"] = field_.default_factory()
else:
defaults_cfg["required"] = True
return defaults_cfg
def _get_choices(field_: Field, base_type):
if issubclass(base_type, Enum):
return {"choices": [e.value for e in base_type]}
elif CHOICES in field_.metadata:
return {"choices": field_.metadata[CHOICES]}
return {}
def _get_help(field_: Field):
if HELP in field_.metadata:
return {"help": field_.metadata[HELP]}
return {}
def _compute_arg_names(name, field_) -> list[str]:
names = [_make_gnu_option(name)] # long option
if SHORTOPT in field_.metadata:
if len(field_.metadata[SHORTOPT]) != 1:
raise ArgclassError(
f"Short option for field {name} must be a single character"
)
names.append(f"-{field_.metadata[SHORTOPT]}")
return names
def _prepare_bool(ap: ArgumentParser, name, field_):
defaults_cfg = _decide_default(field_)
required = defaults_cfg.get("required", False)
bool_parser = ap.add_mutually_exclusive_group(required=required)
if HELP in field_.metadata:
true_help = {"help": f"{field_.metadata[HELP]} (enable)"}
false_help = {"help": f"{field_.metadata[HELP]} (disable)"}
else:
true_help: dict[str, str] = {}
false_help: dict[str, str] = {}
bool_parser.add_argument(
_make_gnu_option(name),
action="store_true",
dest=name,
**true_help, # type: ignore
)
bool_parser.add_argument(
_make_gnu_option(f"no_{name}"),
action="store_false",
dest=name,
**false_help, # type: ignore
)
if not required:
ap.set_defaults(**{name: defaults_cfg["default"]})
def _prepare_list_cfg(name: str, field_: Field):
defaults_cfg = _decide_default(field_)
subtype = typing.get_args(field_.type)
if not subtype:
raise ArgclassError(
f"List field {name} must have a subtype. "
"Did you mean {name}: list[str]?"
)
choices_cfg = _get_choices(field_, subtype[0])
if field_.metadata.get(ALLOW_EMPTY, False):
nargs = "*"
else:
nargs = "+"
return {
**defaults_cfg,
**choices_cfg,
"nargs": nargs,
"type": subtype[0],
}
def _prepare_trivial_cfg(_, field_: Field):
arg_cfg = {
**_decide_default(field_),
**_get_choices(field_, field_.type),
"type": field_.type,
}
return arg_cfg
def _prepare_list(ap: ArgumentParser, name, field_):
arg_cfg = _prepare_list_cfg(name, field_)
arg_names = _compute_arg_names(name, field_)
ap.add_argument(*arg_names, **arg_cfg, **_get_help(field_))
def _prepare_trivial(ap: ArgumentParser, name, field_):
arg_cfg = _prepare_trivial_cfg(name, field_)
arg_names = _compute_arg_names(name, field_)
ap.add_argument(*arg_names, **arg_cfg, **_get_help(field_))
def _prepare_field(ap: ArgumentParser, name: str, field_: Field):
if field_.type is bool:
_prepare_bool(ap, name, field_)
elif typing.get_origin(field_.type) is list:
_prepare_list(ap, name, field_)
else:
_prepare_trivial(ap, name, field_)
def argclass(cls):
@classmethod
def parse_args(cls, argv=None):
ap = ArgumentParser()
for name, field_ in cls.__dataclass_fields__.items():
_prepare_field(ap, name, field_)
return cls(**vars(ap.parse_args(argv)))
cls = dataclass(cls)
cls.parse_args = parse_args
return cls
def argfield(
*,
shortopt: str | None = None,
choices: list[typing.Any] | None = None,
allow_empty: bool | None = None,
help: str | None = None,
**kwargs,
):
metadata = kwargs.pop("metadata", {})
if shortopt is not None:
metadata[SHORTOPT] = shortopt
if choices is not None:
metadata[CHOICES] = choices
if allow_empty is not None:
metadata[ALLOW_EMPTY] = allow_empty
if help is not None:
metadata[HELP] = help
return field(**kwargs, metadata=metadata)

113
test.py
View File

@@ -1,7 +1,9 @@
# type: ignore[ty:unresolved-attribute]
# type: ignore[ty:unknown-argument]
import unittest import unittest
from typing import List from enum import Enum
from argclass import argclass from argclass import argclass, argfield
class TestArgClass(unittest.TestCase): class TestArgClass(unittest.TestCase):
@@ -11,19 +13,7 @@ class TestArgClass(unittest.TestCase):
arg1: str arg1: str
assert A.parse_args(["--arg1", "hello"]) == A(arg1="hello") assert A.parse_args(["--arg1", "hello"]) == A(arg1="hello")
def test__required_argument_missing(self):
@argclass
class A:
arg1: str
self.assertRaises(SystemExit, A.parse_args, []) self.assertRaises(SystemExit, A.parse_args, [])
def test__required_argument_wrong_given(self):
@argclass
class A:
arg1: str
self.assertRaises(SystemExit, A.parse_args, ["--arg2", "hello"]) self.assertRaises(SystemExit, A.parse_args, ["--arg2", "hello"])
def test__optional_argument_missing(self): def test__optional_argument_missing(self):
@@ -32,33 +22,50 @@ class TestArgClass(unittest.TestCase):
arg2: str = "world" arg2: str = "world"
assert A.parse_args([]) == A(arg2="world") assert A.parse_args([]) == A(arg2="world")
def test__optional_argument_given(self):
@argclass
class A:
arg2: str = "world"
assert A.parse_args(["--arg2", "welt"]) == A(arg2="welt") assert A.parse_args(["--arg2", "welt"]) == A(arg2="welt")
def test__optional_argument_wrong_given(self):
@argclass
class A:
arg2: str = "world"
self.assertRaises(SystemExit, A.parse_args, ["--arg3", "welt"]) self.assertRaises(SystemExit, A.parse_args, ["--arg3", "welt"])
def test__boolean_true(self): def test__str_enum_choices(self):
class MyEnum(str, Enum):
hello = "hello"
world = "world"
@argclass
class A:
arg3: MyEnum
assert A.parse_args(["--arg3", "hello"]) == A(arg3=MyEnum.hello)
self.assertRaises(SystemExit, A.parse_args, ["--arg3", "foo"])
def test__str_enum_default(self):
class MyEnum(str, Enum):
hello = "hello"
world = "world"
@argclass
class A:
arg3: MyEnum = MyEnum.hello
assert A.parse_args([]) == A(arg3=MyEnum.hello)
assert A.parse_args(["--arg3", "world"]) == A(arg3=MyEnum.world)
def test__boolean(self):
@argclass @argclass
class A: class A:
arg3: bool arg3: bool
assert A.parse_args(["--arg3"]) == A(arg3=True) assert A.parse_args(["--arg3"]) == A(arg3=True)
assert A.parse_args(["--no-arg3"]) == A(arg3=False)
def test__boolean_false(self): def test__boolean_default_true(self):
@argclass @argclass
class A: class A:
arg3: bool arg3: bool = True
assert A.parse_args([]) == A(arg3=True)
assert A.parse_args(["--arg3"]) == A(arg3=True)
assert A.parse_args(["--no-arg3"]) == A(arg3=False) assert A.parse_args(["--no-arg3"]) == A(arg3=False)
def test__int(self): def test__int(self):
@@ -67,27 +74,12 @@ class TestArgClass(unittest.TestCase):
arg4: int arg4: int
assert A.parse_args(["--arg4", "42"]) == A(arg4=42) assert A.parse_args(["--arg4", "42"]) == A(arg4=42)
def test__int_malformed(self):
@argclass
class A:
arg4: int
self.assertRaises(SystemExit, A.parse_args, ["--arg4", "4e2"]) self.assertRaises(SystemExit, A.parse_args, ["--arg4", "4e2"])
def test__list(self):
@argclass
class A:
arg5: List
assert A.parse_args(["--arg5", "hello", "world"]) == A(
arg5=["hello", "world"]
)
def test__list_str(self): def test__list_str(self):
@argclass @argclass
class A: class A:
arg5: List[str] arg5: list[str]
assert A.parse_args(["--arg5", "hello", "world"]) == A( assert A.parse_args(["--arg5", "hello", "world"]) == A(
arg5=["hello", "world"] arg5=["hello", "world"]
@@ -96,10 +88,39 @@ class TestArgClass(unittest.TestCase):
def test__list_int(self): def test__list_int(self):
@argclass @argclass
class A: class A:
arg5: List[int] arg5: list[int]
assert A.parse_args(["--arg5", "23", "42"]) == A(arg5=[23, 42]) assert A.parse_args(["--arg5", "23", "42"]) == A(arg5=[23, 42])
def test__list_empty(self):
@argclass
class A:
arg5: list[str] = argfield(allow_empty=True)
assert A.parse_args(["--arg5"]) == A(arg5=[])
def test__shortoptions(self):
@argclass
class A:
world: str = argfield(shortopt="w")
assert A.parse_args(["-w", "hello"]) == A(world="hello")
def test__choices_str_valid(self):
@argclass
class A:
world: str = argfield(choices=["hello", "goodbye"])
assert A.parse_args(["--world", "hello"]) == A(world="hello")
self.assertRaises(SystemExit, A.parse_args, ["--world", "foo"])
def test__choices_int(self):
@argclass
class A:
number: int = argfield(choices=[4, 5, 6])
assert A.parse_args(["--number", "5"]) == A(number=5)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

71
uv.lock generated Normal file
View File

@@ -0,0 +1,71 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[package]]
name = "argclass"
version = "0.1.0.dev1"
source = { editable = "." }
[package.dev-dependencies]
dev = [
{ name = "ruff" },
{ name = "ty" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "ruff", specifier = ">=0.15" },
{ name = "ty", specifier = ">=0.0.27" },
]
[[package]]
name = "ruff"
version = "0.15.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" },
{ url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" },
{ url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" },
{ url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" },
{ url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" },
{ url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" },
{ url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" },
{ url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" },
{ url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" },
{ url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" },
{ url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" },
{ url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" },
{ url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" },
{ url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" },
{ url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" },
{ url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" },
]
[[package]]
name = "ty"
version = "0.0.27"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f4/de/e5cf1f151cf52fe1189e42d03d90909d7d1354fdc0c1847cbb63a0baa3da/ty-0.0.27.tar.gz", hash = "sha256:d7a8de3421d92420b40c94fe7e7d4816037560621903964dd035cf9bd0204a73", size = 5424130, upload-time = "2026-03-31T19:07:20.806Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/20/2a9ea661758bd67f2bfd54ce9daacb5a26c56c5f8b49fbd9a43b365a8a7d/ty-0.0.27-py3-none-linux_armv6l.whl", hash = "sha256:eb14456b8611c9e8287aa9b633f4d2a0d9f3082a31796969e0b50bdda8930281", size = 10571211, upload-time = "2026-03-31T19:07:23.28Z" },
{ url = "https://files.pythonhosted.org/packages/da/b2/8887a51f705d075ddbe78ae7f0d4755ef48d0a90235f67aee289e9cee950/ty-0.0.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:02e662184703db7586118df611cf24a000d35dae38d950053d1dd7b6736fd2c4", size = 10427576, upload-time = "2026-03-31T19:07:15.499Z" },
{ url = "https://files.pythonhosted.org/packages/1d/c3/79d88163f508fb709ce19bc0b0a66c7c64b53d372d4caa56172c3d9b3ae8/ty-0.0.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be5fc2899441f7f8f7ef40f9ffd006075a5ff6b06c44e8d2aa30e1b900c12f51", size = 9870359, upload-time = "2026-03-31T19:07:36.852Z" },
{ url = "https://files.pythonhosted.org/packages/dc/4d/ed1b0db0e1e46b5ed4976bbfe0d1825faf003b4e3774ef28c785ed73e4bb/ty-0.0.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30231e652b14742a76b64755e54bf0cb1cd4c128bcaf625222e0ca92a2094887", size = 10380488, upload-time = "2026-03-31T19:07:31.268Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f2/20372f6d510b01570028433064880adec2f8abe68bf0c4603be61a560bef/ty-0.0.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a119b1168f64261b3205a37e40b5b6c4aac8fd58e4587988f4e4b22c3c79847", size = 10390248, upload-time = "2026-03-31T19:07:28.345Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/46b31a7311306be1a560f7f20fdc37b5bf718787f60626cd265d9b637554/ty-0.0.27-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e38f4e187b6975d2cbebf0f1eb1221f8f64f6e509bad14d7bb2a91afc97e4956", size = 10878479, upload-time = "2026-03-31T19:07:39.393Z" },
{ url = "https://files.pythonhosted.org/packages/42/ba/5231a2a1fb1cebe053a25de8fded95e1a30a1e77d3628a9e58487297bafc/ty-0.0.27-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a07b1a8fbb23844f6d22091275430d9ac617175f34aa99159b268193de210389", size = 11461232, upload-time = "2026-03-31T19:07:02.518Z" },
{ url = "https://files.pythonhosted.org/packages/c3/37/558abab3e1f6670493524f61280b4dfcc3219555f13889223e733381dfab/ty-0.0.27-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3ec4033031f240836bb0337274bac5c49dde312c7c6d7575451ed719bf8ffa3", size = 11133002, upload-time = "2026-03-31T19:07:18.371Z" },
{ url = "https://files.pythonhosted.org/packages/32/38/188c14a57f52160407ce62c6abb556011718fd0bcbe1dca690529ce84c46/ty-0.0.27-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:924a8849afd500d260bf5b7296165a05b7424fbb6b19113f30f3b999d682873f", size = 10986624, upload-time = "2026-03-31T19:07:13.066Z" },
{ url = "https://files.pythonhosted.org/packages/9f/f1/667a71393f47d2cd6ba9ed07541b8df3eb63aab1f2ee658e77d91b8362fa/ty-0.0.27-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d8270026c07e7423a1b3a3fd065b46ed1478748f0662518b523b57744f3fa025", size = 10366721, upload-time = "2026-03-31T19:07:00.131Z" },
{ url = "https://files.pythonhosted.org/packages/8b/aa/8edafe41be898bda774249abc5be6edd733e53fb1777d59ea9331e38537d/ty-0.0.27-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e26e9735d3bdfd95d881111ad1cf570eab8188d8c3be36d6bcaad044d38984d8", size = 10412239, upload-time = "2026-03-31T19:07:05.297Z" },
{ url = "https://files.pythonhosted.org/packages/53/ff/8bafaed4a18d38264f46bdfc427de7ea2974cf9064e4e0bdb1b6e6c724e3/ty-0.0.27-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c09cc9a699810609acc0090af8d0db68adaee6e60a7c3e05ab80cc954a83db7", size = 10573507, upload-time = "2026-03-31T19:06:57.064Z" },
{ url = "https://files.pythonhosted.org/packages/16/2e/63a8284a2fefd08ab56ecbad0fde7dd4b2d4045a31cf24c1d1fcd9643227/ty-0.0.27-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2d3e02853bb037221a456e034b1898aaa573e6374fbb53884e33cb7513ccb85a", size = 11090233, upload-time = "2026-03-31T19:07:34.139Z" },
{ url = "https://files.pythonhosted.org/packages/14/d3/d6fa1cafdfa2b34dbfa304fc6833af8e1669fc34e24d214fa76d2a2e5a25/ty-0.0.27-py3-none-win32.whl", hash = "sha256:34e7377f2047c14dbbb7bf5322e84114db7a5f2cb470db6bee63f8f3550cfc1e", size = 9984415, upload-time = "2026-03-31T19:07:07.98Z" },
{ url = "https://files.pythonhosted.org/packages/85/e6/dd4e27da9632b3472d5711ca49dbd3709dbd3e8c73f3af6db9c254235ca9/ty-0.0.27-py3-none-win_amd64.whl", hash = "sha256:3f7e4145aad8b815ed69b324c93b5b773eb864dda366ca16ab8693ff88ce6f36", size = 10961535, upload-time = "2026-03-31T19:07:10.566Z" },
{ url = "https://files.pythonhosted.org/packages/0e/1a/824b3496d66852ed7d5d68d9787711131552b68dce8835ce9410db32e618/ty-0.0.27-py3-none-win_arm64.whl", hash = "sha256:95bf8d01eb96bb2ba3ffc39faff19da595176448e80871a7b362f4d2de58476c", size = 10376689, upload-time = "2026-03-31T19:07:25.732Z" },
]