mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat: Add ty type checker (#38798)
This commit is contained in:
18
.github/ty-problem-matcher.json
vendored
Normal file
18
.github/ty-problem-matcher.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "ty",
|
||||
"severity": "warning",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^([^:]*):(\\d+):(?:(\\d+):)? (?:error|warning): (.*?)(?: \\[(\\S+)\\])?$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"message": 4,
|
||||
"code": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
16
.github/workflows/ci-python.yml
vendored
16
.github/workflows/ci-python.yml
vendored
@@ -89,7 +89,21 @@ jobs:
|
||||
run: |
|
||||
ruff format --check --diff .
|
||||
|
||||
- name: Add Problem Matcher
|
||||
- name: Add ty Problem Matcher
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::add-matcher::.github/ty-problem-matcher.json"
|
||||
|
||||
- name: Check ty type checking (informational)
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "👀 Running ty type checker (informational - does not block CI)"
|
||||
echo "📊 Trial run to evaluate ty vs mypy. Feedback welcome in #team-devex"
|
||||
echo ""
|
||||
./bin/ty.py check posthog ee common dags || echo "⚠️ ty found type issues (annotations below)"
|
||||
|
||||
- name: Add mypy Problem Matcher
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::add-matcher::.github/mypy-problem-matcher.json"
|
||||
|
||||
241
bin/ty.py
Executable file
241
bin/ty.py
Executable file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run ty with mypy-baseline integration.
|
||||
|
||||
This helper mirrors the workflow provided by mypy-baseline's CLI helpers. It
|
||||
supports filtering diagnostics during pre-commit runs and updating the
|
||||
``mypy-baseline.txt`` file from ty's output when new violations are added or
|
||||
removed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
TY_BASELINE_PATH = REPO_ROOT / "ty-baseline.txt"
|
||||
PYPROJECT_PATH = REPO_ROOT / "pyproject.toml"
|
||||
|
||||
# ty prints lines like ``path:line[:column]: error[rule] message``.
|
||||
# Normalizing them makes mypy-baseline treat ty the same way as mypy.
|
||||
_TY_PATTERN = re.compile(
|
||||
r"^(?P<prefix>.+:\d+(?::\d+)?): (?P<severity>error|warn|warning)\[(?P<rule>[^\]]+)\] (?P<message>.*)$"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TyResult:
|
||||
returncode: int
|
||||
output: str
|
||||
|
||||
|
||||
def _load_baseline_paths(baseline_path: Path) -> set[str]:
|
||||
if not baseline_path.exists():
|
||||
return set()
|
||||
paths: set[str] = set()
|
||||
for raw_line in baseline_path.read_text(encoding="utf-8").splitlines():
|
||||
if not raw_line or raw_line.startswith("#"):
|
||||
continue
|
||||
path, *_rest = raw_line.split(":", 1)
|
||||
paths.add(path)
|
||||
return paths
|
||||
|
||||
|
||||
def _should_skip(path: str, *, baseline_paths: set[str]) -> bool:
|
||||
candidate = Path(path)
|
||||
if not candidate.is_absolute():
|
||||
candidate = (Path.cwd() / candidate).resolve()
|
||||
try:
|
||||
relative = candidate.relative_to(REPO_ROOT)
|
||||
except ValueError:
|
||||
relative = candidate
|
||||
return relative.as_posix() in baseline_paths
|
||||
|
||||
|
||||
def _normalize_ty_output(raw_output: str) -> str:
|
||||
normalized_lines: list[str] = []
|
||||
for raw_line in raw_output.splitlines():
|
||||
line = raw_line.rstrip("\n")
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("WARN ty is pre-release"):
|
||||
continue
|
||||
if line.startswith("Checking "):
|
||||
continue
|
||||
if line.startswith("Found ") and "diagnostic" in line:
|
||||
continue
|
||||
if line.startswith("info:"):
|
||||
continue
|
||||
if line.startswith("All checks passed"):
|
||||
continue
|
||||
match = _TY_PATTERN.match(line)
|
||||
if match:
|
||||
severity = match.group("severity")
|
||||
if severity == "warn":
|
||||
severity = "warning"
|
||||
normalized_lines.append(
|
||||
f"{match.group('prefix')}: {severity}: {match.group('message')} [{match.group('rule')}]"
|
||||
)
|
||||
continue
|
||||
normalized_lines.append(line)
|
||||
return "\n".join(normalized_lines)
|
||||
|
||||
|
||||
def _run_ty(targets: Sequence[str]) -> TyResult:
|
||||
if not targets:
|
||||
return TyResult(returncode=0, output="")
|
||||
proc = subprocess.run(
|
||||
["uv", "run", "ty", "check", "--output-format", "concise", *targets],
|
||||
cwd=REPO_ROOT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return TyResult(returncode=proc.returncode, output=proc.stdout)
|
||||
|
||||
|
||||
def _run_mypy_baseline(
|
||||
subcommand: str,
|
||||
*,
|
||||
input_text: str,
|
||||
extra_args: Sequence[str] = (),
|
||||
) -> TyResult:
|
||||
proc = subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"run",
|
||||
"mypy-baseline",
|
||||
subcommand,
|
||||
"--config",
|
||||
str(PYPROJECT_PATH),
|
||||
*extra_args,
|
||||
],
|
||||
cwd=REPO_ROOT,
|
||||
input=input_text,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return TyResult(returncode=proc.returncode, output=proc.stdout)
|
||||
|
||||
|
||||
def _check(paths: Sequence[str], *, from_hook: bool = False) -> int:
|
||||
# Check all files - mypy-baseline filter handles line-level filtering
|
||||
filtered_targets = list(paths)
|
||||
if not filtered_targets:
|
||||
return 0
|
||||
|
||||
ty_result = _run_ty(filtered_targets)
|
||||
normalized = _normalize_ty_output(ty_result.output)
|
||||
|
||||
# If ty found no issues, don't even run mypy-baseline filter
|
||||
if ty_result.returncode == 0 and not normalized.strip():
|
||||
return 0
|
||||
|
||||
baseline_result = _run_mypy_baseline(
|
||||
"filter",
|
||||
input_text=normalized,
|
||||
extra_args=("--hide-stats", "--baseline-path", str(TY_BASELINE_PATH)),
|
||||
)
|
||||
if baseline_result.output:
|
||||
sys.stdout.write(baseline_result.output)
|
||||
if not baseline_result.output.endswith("\n"):
|
||||
sys.stdout.write("\n")
|
||||
|
||||
# If there are filtered errors (baseline_result has output), that means new errors
|
||||
if baseline_result.output:
|
||||
sys.stderr.write(
|
||||
"\n💡 ty found type errors (fast preflight check). For authoritative results, run mypy locally or check CI.\n"
|
||||
)
|
||||
return 1
|
||||
|
||||
if ty_result.returncode != 0 and "error[" not in normalized:
|
||||
# ty failed for a reason unrelated to diagnostics (e.g. crash).
|
||||
sys.stdout.write(ty_result.output)
|
||||
if not ty_result.output.endswith("\n"):
|
||||
sys.stdout.write("\n")
|
||||
return ty_result.returncode
|
||||
|
||||
# All good - no new errors
|
||||
return 0
|
||||
|
||||
|
||||
def _sync(paths: Sequence[str]) -> int:
|
||||
# Check all Python directories if no paths specified
|
||||
targets = list(paths) if paths else ["posthog", "ee", "common", "dags"]
|
||||
ty_result = _run_ty(targets)
|
||||
normalized = _normalize_ty_output(ty_result.output)
|
||||
|
||||
sync_result = _run_mypy_baseline(
|
||||
"sync",
|
||||
input_text=normalized,
|
||||
extra_args=("--hide-stats", "--baseline-path", str(TY_BASELINE_PATH)),
|
||||
)
|
||||
|
||||
if sync_result.output:
|
||||
sys.stdout.write(sync_result.output)
|
||||
if not sync_result.output.endswith("\n"):
|
||||
sys.stdout.write("\n")
|
||||
|
||||
if sync_result.returncode != 0:
|
||||
return sync_result.returncode
|
||||
|
||||
if ty_result.returncode != 0 and "error[" not in normalized:
|
||||
# Preserve failures unrelated to diagnostics so they aren't hidden.
|
||||
sys.stdout.write(ty_result.output)
|
||||
if not ty_result.output.endswith("\n"):
|
||||
sys.stdout.write("\n")
|
||||
return ty_result.returncode
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _parse_args(argv: Sequence[str]) -> tuple[str, list[str], bool]:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run ty with mypy-baseline integration.",
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
check_parser = subparsers.add_parser(
|
||||
"check",
|
||||
help="Run ty on the given paths, filtering diagnostics using mypy-baseline.",
|
||||
)
|
||||
check_parser.add_argument("paths", nargs="*")
|
||||
|
||||
sync_parser = subparsers.add_parser(
|
||||
"sync",
|
||||
help="Update mypy-baseline.txt by re-running ty on the provided paths.",
|
||||
)
|
||||
sync_parser.add_argument("paths", nargs="*")
|
||||
|
||||
# ``bin/ty.py <files>`` should behave like ``bin/ty.py check <files>`` for lint-staged.
|
||||
# When called this way (no subcommand), it's from a hook
|
||||
from_hook: bool = bool(argv and not argv[0].startswith("-") and argv[0] not in {"check", "sync"})
|
||||
if from_hook:
|
||||
argv = ["check", *argv]
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
command: str = cast(str, args.command or "check")
|
||||
paths: list[str] = cast(list[str], getattr(args, "paths", []) or [])
|
||||
return command, paths, from_hook
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
command, paths, from_hook = _parse_args(list(argv) if argv is not None else sys.argv[1:])
|
||||
if command == "check":
|
||||
return _check(paths, from_hook=from_hook)
|
||||
if command == "sync":
|
||||
return _sync(paths)
|
||||
raise AssertionError(f"Unknown command: {command}")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover - CLI entry point
|
||||
sys.exit(main())
|
||||
75
docs/ty-baseline.md
Normal file
75
docs/ty-baseline.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Ty Type Checking
|
||||
|
||||
ty runs in CI as an informational check to evaluate its usefulness vs mypy. If you see ty annotations in your PR:
|
||||
|
||||
- 👀 Review the feedback - ty often catches real type issues
|
||||
- 📊 Share your experience in #team-devex
|
||||
- ⚠️ ty warnings are informational and don't block CI
|
||||
|
||||
The baseline system filters out 567 pre-existing errors so you only see new issues introduced by your changes.
|
||||
|
||||
## Manual Usage
|
||||
|
||||
```bash
|
||||
./bin/ty.py check path/to/file.py # Check specific files
|
||||
./bin/ty.py check posthog ee # Check directories
|
||||
```
|
||||
|
||||
## Ty vs mypy: Fast trial vs authoritative checking
|
||||
|
||||
**ty** is currently in trial mode:
|
||||
|
||||
- ⚡ Extremely fast (~10-100x faster than mypy)
|
||||
- 🧪 Alpha software (v0.0.1a22 - expect edge cases)
|
||||
- 📢 Runs in CI only (non-blocking) to gather feedback
|
||||
- 🚨 Uses GitHub problem matcher to show warnings inline
|
||||
|
||||
**mypy** remains the **authoritative type checker**:
|
||||
|
||||
- 🎯 More mature and comprehensive
|
||||
- 🐌 Slower but thorough
|
||||
- ✅ Final source of truth (runs in CI and blocks on errors)
|
||||
- 📍 Runs in CI and recommended for local deep checks
|
||||
|
||||
This trial helps us evaluate whether ty should become a blocking check in the future.
|
||||
|
||||
## Baseline Management
|
||||
|
||||
### Two Separate Baselines
|
||||
|
||||
- **`mypy-baseline.txt`** - Maintained by mypy (~1287 errors)
|
||||
- **`ty-baseline.txt`** - Maintained by ty (567 diagnostics)
|
||||
|
||||
ty maintains its own baseline because it reports different errors than mypy. The baseline contains pre-existing errors that won't trigger warnings in CI.
|
||||
|
||||
### Baseline Contents
|
||||
|
||||
The ty-baseline.txt contains:
|
||||
|
||||
- 91 redundant-cast (easy cleanup opportunities)
|
||||
- 96 possibly-unbound-attribute (null safety checks)
|
||||
- 119 missing-argument (real bugs requiring investigation)
|
||||
- 52 deprecated warnings
|
||||
- Plus other categories
|
||||
|
||||
About 50% are real bugs, 25% safety improvements, 25% trivial cleanups.
|
||||
|
||||
### Updating ty-baseline
|
||||
|
||||
When you fix ty errors across the codebase, update the baseline:
|
||||
|
||||
```bash
|
||||
./bin/ty.py sync
|
||||
git add ty-baseline.txt
|
||||
git commit -m "chore: update ty baseline after fixing type errors"
|
||||
```
|
||||
|
||||
The sync command runs ty on all Python directories and updates `ty-baseline.txt` with the current state.
|
||||
|
||||
### How Filtering Works in CI
|
||||
|
||||
1. CI runs ty on your changed files
|
||||
2. ty finds errors (both old and new)
|
||||
3. Baseline filter removes errors already in `ty-baseline.txt`
|
||||
4. Only new errors you introduced are shown as warnings
|
||||
5. CI always passes (ty is informational only)
|
||||
@@ -195,6 +195,7 @@ dev = [
|
||||
"mypy~=1.18.2",
|
||||
"mypy-baseline~=0.7.3",
|
||||
"mypy-extensions~=1.1.0",
|
||||
"ty==0.0.1a22",
|
||||
# openapi-spec-validator needed for prance as a validation backend
|
||||
"openapi-spec-validator==0.7.1",
|
||||
"orjson==3.10.15",
|
||||
@@ -341,3 +342,24 @@ python_version = "3.12"
|
||||
[tool.mypy-baseline]
|
||||
sort_baseline = true
|
||||
ignore_categories = ["note"]
|
||||
|
||||
[tool.ty.environment]
|
||||
python-version = "3.12"
|
||||
|
||||
[tool.ty.src]
|
||||
exclude = [
|
||||
"bin/**",
|
||||
"gunicorn.config.py",
|
||||
"manage.py",
|
||||
"posthog/hogql/grammar/**",
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
# Import resolution - ty can't always find all modules/stubs
|
||||
unresolved-import = "ignore"
|
||||
|
||||
# Django-related rules - ty doesn't support Django's metaprogramming yet
|
||||
# See: https://github.com/astral-sh/ty/issues/291
|
||||
unresolved-attribute = "ignore" # Django model fields, relationships, auto-generated attributes
|
||||
invalid-argument-type = "ignore" # Django querysets, managers, form fields
|
||||
invalid-base = "ignore" # Django model metaclasses (e.g., models.Model)
|
||||
|
||||
502
ty-baseline.txt
Normal file
502
ty-baseline.txt
Normal file
File diff suppressed because one or more lines are too long
29
uv.lock
generated
29
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = "==3.12.11"
|
||||
|
||||
[[package]]
|
||||
@@ -4312,6 +4312,7 @@ dev = [
|
||||
{ name = "stpyv8", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64') or sys_platform != 'linux'" },
|
||||
{ name = "syrupy" },
|
||||
{ name = "tach" },
|
||||
{ name = "ty" },
|
||||
{ name = "types-aioboto3", extra = ["s3"] },
|
||||
{ name = "types-aiobotocore" },
|
||||
{ name = "types-boto3", extra = ["essential"] },
|
||||
@@ -4548,6 +4549,7 @@ dev = [
|
||||
{ name = "stpyv8", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64') or sys_platform != 'linux'", specifier = "==13.1.201.22" },
|
||||
{ name = "syrupy", specifier = "~=4.9.1" },
|
||||
{ name = "tach", specifier = "~=0.20.0" },
|
||||
{ name = "ty", specifier = "==0.0.1a22" },
|
||||
{ name = "types-aioboto3", extras = ["s3"], specifier = ">=14.3.0" },
|
||||
{ name = "types-aiobotocore", specifier = ">=2.22.0" },
|
||||
{ name = "types-boto3", extras = ["essential"], specifier = "==1.37.6" },
|
||||
@@ -6443,6 +6445,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.1a22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/87/eab73cdc990d1141b60237379975efc0e913bfa0d19083daab0f497444a6/ty-0.0.1a22.tar.gz", hash = "sha256:b20ec5362830a1e9e05654c15e88607fdbb45325ec130a9a364c6dd412ecbf55", size = 4312182, upload-time = "2025-10-10T13:07:15.88Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/30/83e2dbfbc70de8a1932b19daf05ce803d7d76cdc6251de1519a49cf1c27d/ty-0.0.1a22-py3-none-linux_armv6l.whl", hash = "sha256:6efba0c777881d2d072fa7375a64ad20357e825eff2a0b6ff9ec80399a04253b", size = 8581795, upload-time = "2025-10-10T13:06:44.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/8c/5193534fc4a3569f517408828d077b26d6280fe8c2dd0bdc63db4403dcdb/ty-0.0.1a22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2ada020eebe1b44403affdf45cd5c8d3fb8312c3e80469d795690093c0921f55", size = 8682602, upload-time = "2025-10-10T13:06:46.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/4a/7ba53493bf37b61d3e0dfe6df910e6bc74c40d16c3effd84e15c0863d34e/ty-0.0.1a22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ed4f11f1a5824ea10d3e46b1990d092c3f341b1d492c357d23bed2ac347fd253", size = 8278839, upload-time = "2025-10-10T13:06:48.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/0a/d9862c41b9615de56d2158bfbb5177dbf5a65e94922d3dd13855f48cb91b/ty-0.0.1a22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56f48d8f94292909d596dbeb56ff7f9f070bd316aa628b45c02ca2b2f5797f31", size = 8421483, upload-time = "2025-10-10T13:06:50.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/cb/3ebe0e45b80724d4c2f849fdf304179727fd06df7fee7cd12fe6c3efe49d/ty-0.0.1a22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:733e9ac22885b6574de26bdbae439c960a06acc825a938d3780c9d498bb65339", size = 8419225, upload-time = "2025-10-10T13:06:52.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b5/da65f3f8ad31d881ca9987a3f6f26069a0cc649c9354adb7453ca62116bb/ty-0.0.1a22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5135d662484e56809c77b3343614005585caadaa5c1cf643ed6a09303497652b", size = 9352336, upload-time = "2025-10-10T13:06:54.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/24/9c46f2eb16734ab0fcf3291486b1c5c528a1569f94541dc1f19f97dd2a5b/ty-0.0.1a22-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:87f297f99a98154d33a3f21991979418c65d8bf480f6a1bad1e54d46d2dc7df7", size = 9857840, upload-time = "2025-10-10T13:06:56.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/ae/930c94bbbe5c049eae5355a197c39522844f55c7ab7fccd0ba061f618541/ty-0.0.1a22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3310217eaa4dccf20b7336fcbeb072097addc6fde0c9d3f791dea437af0aa6dc", size = 9452611, upload-time = "2025-10-10T13:06:58.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/80/d8f594438465c352cf0ebd4072f5ca3be2871153a3cd279ed2f35ecd487c/ty-0.0.1a22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12b032e81012bf5228fd65f01b50e29eb409534b6aac28ee5c48ee3b7b860ddf", size = 9214875, upload-time = "2025-10-10T13:06:59.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/07/f852fb20ac27707de495c39a02aeb056e3368833b7e12888d43b1f61594d/ty-0.0.1a22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3ffda8149cab0000a21e7a078142073e27a1a9ac03b9a0837aa2f53d1fbebcb", size = 8906715, upload-time = "2025-10-10T13:07:01.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/4d/0e0b85b4179891cc3067a6e717f5161921c07873a4f545963fdf1dd3619c/ty-0.0.1a22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:afa512e7dc78f0cf0b55f87394968ba59c46993c67bc0ef295962144fea85b12", size = 8350873, upload-time = "2025-10-10T13:07:03.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/1f/e70c63e12b4a0d97d4fd6f872dd199113666ad1b236e18838fa5e5d5502d/ty-0.0.1a22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:069cdbbea6025f7ebbb5e9043c8d0daf760358df46df8304ef5ca5bb3e320aef", size = 8442568, upload-time = "2025-10-10T13:07:05.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/3b/55518906cb3598f2b99ff1e86c838d77d006cab70cdd2a0a625d02ccb52c/ty-0.0.1a22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:67d31d902e6fd67a4b3523604f635e71d2ec55acfb9118f984600584bfe0ff2a", size = 8896775, upload-time = "2025-10-10T13:07:08.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/ea/60c654c27931bf84fa9cb463a4c4c49e8869c052fa607a6e930be717b619/ty-0.0.1a22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f9e154f262162e6f76b01f318e469ac6c22ffce22b010c396ed34e81d8369821", size = 9054544, upload-time = "2025-10-10T13:07:09.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/60/9a6d5530d6829ccf656e6ae0fb13d70a4e2514f4fb8910266ebd54286620/ty-0.0.1a22-py3-none-win32.whl", hash = "sha256:37525433ca7b02a8fca4b8fa9dcde818bf3a413b539b9dbc8f7b39d124eb7c49", size = 8165703, upload-time = "2025-10-10T13:07:11.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/9c/ac08c832643850d4e18cbc959abc69cd51d531fe11bdb691098b3cf2f562/ty-0.0.1a22-py3-none-win_amd64.whl", hash = "sha256:75d21cdeba8bcef247af89518d7ce98079cac4a55c4160cb76682ea40a18b92c", size = 8828319, upload-time = "2025-10-10T13:07:12.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/df/38068fc44e3cfb455aeb41d0ff1850a4d3c9988010466d4a8d19860b8b9a/ty-0.0.1a22-py3-none-win_arm64.whl", hash = "sha256:1c7f040fe311e9696917417434c2a0e58402235be842c508002c6a2eff1398b0", size = 8367136, upload-time = "2025-10-10T13:07:14.518Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.16.0"
|
||||
|
||||
Reference in New Issue
Block a user