From 91274100ef154cd7e42dbc1277b2de639c04b64e Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 14 May 2026 09:57:26 +0200 Subject: [PATCH 1/2] feat: add python-package-check composite action --- .github/workflows/local_lint.yaml | 23 ++ .gitignore | 3 + git-cliff-release/enhance_context.py | 67 ++--- pyproject.toml | 53 ++++ python-package-check/action.yaml | 54 ++++ python-package-check/verify_built_package.py | 292 +++++++++++++++++++ uv.lock | 72 +++++ 7 files changed, 528 insertions(+), 36 deletions(-) create mode 100644 pyproject.toml create mode 100644 python-package-check/action.yaml create mode 100755 python-package-check/verify_built_package.py create mode 100644 uv.lock diff --git a/.github/workflows/local_lint.yaml b/.github/workflows/local_lint.yaml index 96fbe39a..68182da8 100644 --- a/.github/workflows/local_lint.yaml +++ b/.github/workflows/local_lint.yaml @@ -41,3 +41,26 @@ jobs: - name: Test run: pnpm run actions:test + python_checks: + runs-on: ubuntu-26.04 + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up uv package manager + uses: astral-sh/setup-uv@v8.1.0 + with: + python-version: "3.14" + enable-cache: true + + - name: Install Python dev dependencies + run: uv sync --group dev + + - name: Ruff format + run: uv run ruff format --check + + - name: Ruff lint + run: uv run ruff check + + - name: Type check + run: uv run ty check diff --git a/.gitignore b/.gitignore index aae3fdc4..7399cb6b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ .mise.toml node_modules/ *.tsbuildinfo +__pycache__/ +.ruff_cache/ +.ty_cache/ diff --git a/git-cliff-release/enhance_context.py b/git-cliff-release/enhance_context.py index 6bfaa0a5..4c4d199d 100644 --- a/git-cliff-release/enhance_context.py +++ b/git-cliff-release/enhance_context.py @@ -1,9 +1,9 @@ from __future__ import annotations -from argparse import ArgumentParser, BooleanOptionalAction import json import subprocess import sys +from argparse import ArgumentParser, BooleanOptionalAction from pathlib import Path from typing import Any @@ -11,7 +11,7 @@ def load_pr_issues(owner: str, repo: str) -> dict[int, list[int]]: output = subprocess.check_output( [ - str(Path(__file__).parent / "fetch_pr_issues.sh"), + str(Path(__file__).parent / 'fetch_pr_issues.sh'), owner, repo, ] @@ -20,7 +20,7 @@ def load_pr_issues(owner: str, repo: str) -> dict[int, list[int]]: try: pr_issues = json.loads(output) except ValueError: - print(f"fetch_pr_issues.sh output: {output}") + print(f'fetch_pr_issues.sh output: {output}') raise if pr_issues is None: @@ -29,65 +29,60 @@ def load_pr_issues(owner: str, repo: str) -> dict[int, list[int]]: return {int(key): value for key, value in pr_issues.items()} -def enhance_release( - release: dict[str, Any], is_release_notes: bool, unreleased_version: str | None -) -> None: - release["extra"] = release["extra"] or {} - release["extra"]["is_release_notes"] = is_release_notes +def enhance_release(release: dict[str, Any], *, is_release_notes: bool, unreleased_version: str | None) -> None: + release['extra'] = release['extra'] or {} + release['extra']['is_release_notes'] = is_release_notes - if release["version"]: - release["extra"]["release_link"] = ( - f"{repo_url}/releases/tag/{release['version']}" - ) + if release['version']: + release['extra']['release_link'] = f'{repo_url}/releases/tag/{release["version"]}' elif unreleased_version: - release["extra"]["unreleased_version"] = unreleased_version + release['extra']['unreleased_version'] = unreleased_version def enhance_commit(commit: dict[str, Any], pr_issues: dict[int, list[int]]) -> None: - commit_remote = commit.get("remote", {}) + commit_remote = commit.get('remote', {}) - pr_number = commit_remote.get("pr_number") - username = commit_remote.get("username") + pr_number = commit_remote.get('pr_number') + username = commit_remote.get('username') - commit["extra"] = commit["extra"] or {} - commit["extra"]["commit_link"] = f"{repo_url}/commit/{commit['id']}" + commit['extra'] = commit['extra'] or {} + commit['extra']['commit_link'] = f'{repo_url}/commit/{commit["id"]}' if username: - commit["extra"]["username"] = username + commit['extra']['username'] = username if pr_number: - commit["extra"]["closed_issues"] = pr_issues.get(pr_number, []) + commit['extra']['closed_issues'] = pr_issues.get(pr_number, []) - pr_link = f"{repo_url}/pull/{pr_number}" - commit["extra"]["pr_link"] = f"([#{pr_number}]({pr_link}))" - commit["extra"]["raw_pr_link"] = f"(#{pr_number})" + pr_link = f'{repo_url}/pull/{pr_number}' + commit['extra']['pr_link'] = f'([#{pr_number}]({pr_link}))' + commit['extra']['raw_pr_link'] = f'(#{pr_number})' - commit["extra"]["closed_issue_links"] = [ - f"[#{issue}]({repo_url}/issues/{issue})" - for issue in commit["extra"]["closed_issues"] + commit['extra']['closed_issue_links'] = [ + f'[#{issue}]({repo_url}/issues/{issue})' for issue in commit['extra']['closed_issues'] ] parser = ArgumentParser() -parser.add_argument("--repo", type=str, required=True) -parser.add_argument("--unreleased-version", nargs="?", default=None, type=str) -parser.add_argument("--release-notes", action=BooleanOptionalAction) -parser.add_argument("--no-github", default=False, action="store_true") +parser.add_argument('--repo', type=str, required=True) +parser.add_argument('--unreleased-version', nargs='?', default=None, type=str) +parser.add_argument('--release-notes', action=BooleanOptionalAction) +parser.add_argument('--no-github', default=False, action='store_true') -if __name__ == "__main__": +if __name__ == '__main__': args = parser.parse_args() - repo_url = f"https://github.com/{args.repo}" - owner, repo = args.repo.split("/") + repo_url = f'https://github.com/{args.repo}' + owner, repo = args.repo.split('/') pr_issues = load_pr_issues(owner, repo) context = json.load(sys.stdin) - if not args.no_github: + if not args.no_github: for release in context: - enhance_release(release, args.release_notes, args.unreleased_version) + enhance_release(release, is_release_notes=args.release_notes, unreleased_version=args.unreleased_version) - for commit in release["commits"]: + for commit in release['commits']: enhance_commit(commit, pr_issues) json.dump(context, sys.stdout) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..da0ff001 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[project] +name = "apify-workflows" +version = "0" +description = "Lint and type-check configuration for Python scripts in apify/workflows." +requires-python = ">=3.14" + +[dependency-groups] +dev = [ + "ruff~=0.15.0", + "ty~=0.0.0", +] + +[tool.uv] +package = false + +[tool.ruff] +line-length = 120 +include = ["**/*.py"] + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "COM812", # May conflict with the formatter. + "D", # pydocstyle - docstring presence/formatting not enforced for this repo's small scripts. + "EM", # flake8-errmsg. + "G004", # Logging statement uses f-string. + "ISC001", # May conflict with the formatter. + "PLR0913", # Too many arguments in function definition. + "TD002", # Missing author in TODO. + "TRY003", # Avoid specifying long messages outside the exception class. +] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" + +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "single" + +[tool.ruff.lint.per-file-ignores] +"{python-package-check,git-cliff-release}/**/*.py" = [ + "INP001", # Implicit namespace package - these are script directories, not packages. + "S603", # `subprocess` call: arguments are trusted (CI-provided or constants). + "S607", # Starting a process with a partial executable path - tools resolved from PATH by design. + "T201", # `print` is the script's UX. +] + +[tool.ty.environment] +python-version = "3.14" + +[tool.ty.src] +include = ["python-package-check", "git-cliff-release"] diff --git a/python-package-check/action.yaml b/python-package-check/action.yaml new file mode 100644 index 00000000..548cf510 --- /dev/null +++ b/python-package-check/action.yaml @@ -0,0 +1,54 @@ +name: Python package check +description: > + Verify built sdist + wheel artifacts in `dist/` install and import correctly. + Run after building the package (e.g. via `prepare-pypi-distribution`). + +inputs: + package_name: + description: Importable Python package name (e.g. `crawlee`, `apify`, `apify_client`). + required: true + src_package_dir: + description: Path to the package source directory, relative to the repository root. + required: true + dist_dir: + description: Directory containing the built sdist + wheel. + required: true + python_version: + description: Python version to use for the verification venvs. + required: true + extras: + description: Optional extras to install (e.g. `all`). Empty for no extras. + required: false + default: "" + smoke_code: + description: > + Optional extra Python code to run inside the install smoke test, after `import `. + Useful for asserting that specific symbols import and construct cleanly. + required: false + default: "" + +runs: + using: composite + steps: + - name: Set up uv package manager + uses: astral-sh/setup-uv@v8.1.0 + with: + python-version: ${{ inputs.python_version }} + + - name: Verify built package + shell: bash + env: + PACKAGE_NAME: ${{ inputs.package_name }} + DIST_DIR: ${{ inputs.dist_dir }} + PYTHON_VERSION: ${{ inputs.python_version }} + EXTRAS: ${{ inputs.extras }} + SRC_PACKAGE_DIR: ${{ inputs.src_package_dir }} + SMOKE_CODE: ${{ inputs.smoke_code }} + run: | + uv run --no-project python "${{ github.action_path }}/verify_built_package.py" \ + --package "$PACKAGE_NAME" \ + --dist-dir "$DIST_DIR" \ + --python-version "$PYTHON_VERSION" \ + --extras "$EXTRAS" \ + --src-package-dir "$SRC_PACKAGE_DIR" \ + --smoke-code "$SMOKE_CODE" diff --git a/python-package-check/verify_built_package.py b/python-package-check/verify_built_package.py new file mode 100755 index 00000000..361eb762 --- /dev/null +++ b/python-package-check/verify_built_package.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python + +"""Verify that built artifacts (sdist + wheel) in `dist/` install and import correctly. + +Designed to run against any Python package built via `uv build` / hatch / similar. +Used both locally and in CI by the `apify/workflows/python-package-check` composite action. + +Checks performed: + +* `dist/` contains exactly one `.whl` and one `.tar.gz`. +* Sdist includes all expected source/data/metadata files and excludes `tests/`, `docs/`, `website/`, `examples/`, + `.github/`, and `uv.lock`. +* Wheel includes all expected source/data files and a `*.dist-info/METADATA` entry. +* Wheel installs into a fresh venv and the package imports. +* Sdist installs into a fresh venv (forces pip to rebuild the wheel from sdist contents) and the package imports. +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +import tarfile +import tempfile +import zipfile +from pathlib import Path +from typing import Literal + +REQUIRED_METADATA_FILES = ( + 'LICENSE', + 'README.md', + 'CHANGELOG.md', + 'CONTRIBUTING.md', + 'pyproject.toml', +) + +FORBIDDEN_SDIST_TOPLEVEL_DIRS = ( + 'tests', + 'docs', + 'website', + 'examples', + '.github', +) + +FORBIDDEN_SDIST_FILES = ('uv.lock',) + + +def passed(msg: str) -> None: + print(f'PASS {msg}', flush=True) + + +def failed(msg: str) -> None: + print(f'FAIL {msg}', flush=True) + + +def info(msg: str) -> None: + print(f' {msg}', flush=True) + + +def section(title: str) -> None: + print(f'\n=== {title} ===') + + +def find_artifacts(dist_dir: Path) -> tuple[Path, Path]: + if not dist_dir.is_dir(): + raise SystemExit(f'dist directory not found: {dist_dir}') + wheels = sorted(dist_dir.glob('*.whl')) + sdists = sorted(dist_dir.glob('*.tar.gz')) + if len(wheels) != 1: + raise SystemExit(f'Expected exactly one .whl in {dist_dir}, found {len(wheels)}: {wheels}') + if len(sdists) != 1: + raise SystemExit(f'Expected exactly one .tar.gz in {dist_dir}, found {len(sdists)}: {sdists}') + return wheels[0], sdists[0] + + +def list_sdist_members(sdist: Path) -> list[str]: + prefix = sdist.name.removesuffix('.tar.gz') + '/' + with tarfile.open(sdist, 'r:gz') as tar: + return [m.name.removeprefix(prefix) for m in tar.getmembers() if m.isfile() and m.name.startswith(prefix)] + + +def list_wheel_members(wheel: Path) -> list[str]: + with zipfile.ZipFile(wheel) as zf: + return [n for n in zf.namelist() if not n.endswith('/')] + + +def collect_repo_files(src_package_dir: Path) -> tuple[list[str], list[str]]: + """Return (source_files, data_files) relative to the parent of `src_package_dir`. + + The relative-to-parent layout matches both sdist (`src//...`) and wheel (`/...`). + Data files are any non-`.py` file that isn't a compiled artifact or cache. + """ + if not src_package_dir.is_dir(): + raise SystemExit(f'Source package directory not found: {src_package_dir}') + src_root = src_package_dir.parent + source: list[str] = [] + data: list[str] = [] + for path in src_package_dir.rglob('*'): + if not path.is_file(): + continue + if '__pycache__' in path.parts or path.suffix in ('.pyc', '.pyo'): + continue + rel = path.relative_to(src_root).as_posix() + if path.suffix == '.py': + source.append(rel) + else: + data.append(rel) + return sorted(source), sorted(data) + + +def _preview(items: list[str], limit: int = 5) -> str: + return ', '.join(items[:limit]) + ('...' if len(items) > limit else '') + + +def _check_files_present( + member_set: set[str], + required: list[str], + prefix: str, + label: str, + category: str, +) -> bool: + missing = [r for r in required if f'{prefix}{r}' not in member_set] + if missing: + failed(f'{label} missing {len(missing)} {category} file(s): {_preview(missing)}') + return False + passed(f'{label} has all {len(required)} {category} files') + return True + + +def check_sdist_contents(members: list[str], source_files: list[str], data_files: list[str]) -> bool: + section('Checking sdist contents') + member_set = set(members) + results: list[bool] = [] + + for meta in REQUIRED_METADATA_FILES: + if meta in member_set: + passed(f'sdist has {meta}') + results.append(True) + else: + failed(f'sdist missing {meta}') + results.append(False) + + for forbidden in FORBIDDEN_SDIST_TOPLEVEL_DIRS: + leaked = [m for m in members if m.startswith(f'{forbidden}/')] + if leaked: + failed(f'sdist leaked {forbidden}/ files: {_preview(leaked, limit=3)}') + results.append(False) + else: + passed(f'sdist has no {forbidden}/ leak') + results.append(True) + + for forbidden in FORBIDDEN_SDIST_FILES: + if forbidden in member_set: + failed(f'sdist contains forbidden file {forbidden}') + results.append(False) + else: + passed(f'sdist has no {forbidden}') + results.append(True) + + results.append(_check_files_present(member_set, source_files, 'src/', 'sdist', '.py source')) + if data_files: + results.append(_check_files_present(member_set, data_files, 'src/', 'sdist', 'data')) + return all(results) + + +def check_wheel_contents(members: list[str], source_files: list[str], data_files: list[str]) -> bool: + section('Checking wheel contents') + member_set = set(members) + results: list[bool] = [] + + has_metadata = any(m.endswith('/METADATA') and '.dist-info/' in m for m in members) + if has_metadata: + passed('wheel has .dist-info/METADATA') + results.append(True) + else: + failed('wheel missing .dist-info/METADATA') + results.append(False) + + results.append(_check_files_present(member_set, source_files, '', 'wheel', '.py source')) + if data_files: + results.append(_check_files_present(member_set, data_files, '', 'wheel', 'data')) + return all(results) + + +def install_and_smoke_test( + artifact: Path, + kind: Literal['wheel', 'sdist'], + venv_dir: Path, + package_name: str, + python_version: str, + extras: str, + smoke_code: str, +) -> bool: + section(f'Installing {kind} into fresh venv') + subprocess.run(['uv', 'venv', '--quiet', '--python', python_version, str(venv_dir)], check=True) + python = venv_dir / 'bin' / 'python' + spec = f'{artifact}[{extras}]' if extras else str(artifact) + res = subprocess.run( + ['uv', 'pip', 'install', '--quiet', '--python', str(python), spec], + capture_output=True, + text=True, + check=False, + ) + if res.returncode != 0: + failed(f'{kind} install failed') + info(res.stderr.strip() or res.stdout.strip()) + return False + passed(f'{kind} installed into {venv_dir}') + + base_smoke = f'import {package_name}\nprint(getattr({package_name}, "__version__", ""))\n' + code = base_smoke + (smoke_code or '') + res = subprocess.run([str(python), '-c', code], capture_output=True, text=True, check=False) + if res.returncode != 0: + failed(f'{kind} import smoke test failed') + info(res.stderr.strip()) + return False + version = next(iter(res.stdout.strip().splitlines()), '') + passed(f'{kind} imports OK ({package_name}=={version})') + return True + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--package', required=True, help='Importable Python package name (e.g. crawlee).') + parser.add_argument('--dist-dir', type=Path, default=Path('dist'), help='Directory containing built artifacts.') + parser.add_argument( + '--src-package-dir', + default='', + help='Path to the package source directory. Default: src/.', + ) + parser.add_argument('--extras', default='', help='Optional install extras (e.g. all).') + parser.add_argument('--python-version', default='3.14', help='Python version for verification venvs.') + parser.add_argument( + '--smoke-code', + default='', + help='Optional extra Python code to run after `import ` in the smoke test.', + ) + args = parser.parse_args(argv) + + src_package_dir = (Path(args.src_package_dir) if args.src_package_dir else Path('src') / args.package).resolve() + + wheel, sdist = find_artifacts(args.dist_dir.resolve()) + info(f'package: {args.package}') + info(f'src dir: {src_package_dir}') + info(f'wheel: {wheel.name}') + info(f'sdist: {sdist.name}') + + sdist_members = list_sdist_members(sdist) + wheel_members = list_wheel_members(wheel) + source_files, data_files = collect_repo_files(src_package_dir) + info(f'sources: {len(source_files)}') + info(f'data: {len(data_files)}') + + results: list[bool] = [] + results.append(check_sdist_contents(sdist_members, source_files, data_files)) + results.append(check_wheel_contents(wheel_members, source_files, data_files)) + + with tempfile.TemporaryDirectory(prefix='verify-built-package-') as tmp: + tmp_path = Path(tmp) + results.append( + install_and_smoke_test( + wheel, + 'wheel', + tmp_path / 'venv-wheel', + args.package, + args.python_version, + args.extras, + args.smoke_code, + ) + ) + results.append( + install_and_smoke_test( + sdist, + 'sdist', + tmp_path / 'venv-sdist', + args.package, + args.python_version, + args.extras, + args.smoke_code, + ) + ) + + section('Summary') + if all(results): + passed('all checks passed') + return 0 + failed(f'{sum(1 for r in results if not r)} of {len(results)} check group(s) failed') + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..e4c65b78 --- /dev/null +++ b/uv.lock @@ -0,0 +1,72 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "apify-workflows" +version = "0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "ruff", specifier = "~=0.15.0" }, + { name = "ty", specifier = "~=0.0.0" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[package]] +name = "ty" +version = "0.0.35" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/53/440e7b1212c4b0abbd4adb7aed93f4971aa1f8dca386ac5515930afa9172/ty-0.0.35.tar.gz", hash = "sha256:8375c240ab38138a19db07996c9808fb7a92047c1492e1ce587c2ef5112ad3a9", size = 5629237, upload-time = "2026-05-10T18:25:17.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/84/19662ee881675815b7fafff940a365be1985730465afd9b75cb2edd5f8b3/ty-0.0.35-py3-none-linux_armv6l.whl", hash = "sha256:85ae1e59b9fb0b40e9d84fe61b29653c5f2f5e78b487ece371a7a38c20c781cf", size = 11198741, upload-time = "2026-05-10T18:24:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/62/df/7e5b6f83d85b4d2e5b72b5dceb388f440acc10679417bd46f829b9200fab/ty-0.0.35-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:709dbb7af4fcadb1196863c00b8791bbbbcc9dacbe15a0ff17f0af82b35d415b", size = 10948304, upload-time = "2026-05-10T18:24:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/59/94/72d7263aca055cde427f0ebcf08d6a74e5a5fee1d1e7fdd553696089cecb/ty-0.0.35-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2cb0877419ab0c8708b6925cb0c2800b263842bd3c425113f200538772f3a0cc", size = 10407413, upload-time = "2026-05-10T18:24:37.422Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/fda6fae8a81ce0cb5f24cdfe63260e110c7af8844e31fa07d1e6e8ef0232/ty-0.0.35-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7afbcfc61904b7e82e7fe1a1db832a40d8f01e69dee1775f6594e552980536c", size = 10932614, upload-time = "2026-05-10T18:24:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/72/3d/b98d8d4aa1a5ed6daaf15864e838f605ca7b1e8b93b7e17b96ed4bc4dfed/ty-0.0.35-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b61498cc3e4178031c079951257fbdb209a891b4feb10ad6c40f615a51846f41", size = 10962982, upload-time = "2026-05-10T18:24:44.88Z" }, + { url = "https://files.pythonhosted.org/packages/18/c4/2881aad71bf6fb2f8df17fc8e4bc89e904e54490a3ee747b5ef73f98ac85/ty-0.0.35-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573b1eacda349fc8dba0d767b41631c3a6f66412363127c5bf2b1b40a1d898d2", size = 11476274, upload-time = "2026-05-10T18:24:42.4Z" }, + { url = "https://files.pythonhosted.org/packages/34/0f/7717650adaeaddd23eea70470e2c26d3f0b9b18fdc7f26ec9552d6001f17/ty-0.0.35-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7209746158d6393c1040aa64b3ca29622e212ea7d8bae22ba50dbcbb4f96f0a", size = 12012027, upload-time = "2026-05-10T18:25:00.752Z" }, + { url = "https://files.pythonhosted.org/packages/22/c9/1a16cb4aab6f4707d8f550772e91abc26d1c8870f19b5e2453ad10bb8209/ty-0.0.35-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4466a1470aa4418d49a9aa45d9da7de42033addd0a2837c5b2b0eb71d3c2bcd3", size = 11648894, upload-time = "2026-05-10T18:25:12.44Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/a977c0e07e9f88db9c67f90c6342a4dc4422c8091fa07bf26521870687c5/ty-0.0.35-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb44bb742d52c309dcaa6598bcf4d82eb4bf1241b9e4940461e522e30093fe8b", size = 11560482, upload-time = "2026-05-10T18:25:05.172Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c1/a5fb11227d5cc4ac3f29a115d8c8bc817578e8ef6907d1e4c914ddbf45ee/ty-0.0.35-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:34b219250736c989b2670a03782c61315f523f3a2be37f1f90b1207e2212c188", size = 11718495, upload-time = "2026-05-10T18:24:54.12Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cb/e92e4317388b6d1fd821a46941b448a8a1ff0bf13e22147c5167d8fa1b00/ty-0.0.35-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88e2ac497decc0940ef1a07571dee8a746112a93a09cdc7f8bca0099752e2e05", size = 10900815, upload-time = "2026-05-10T18:25:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4f/03bd87388a92567f262f35ac64e10d2be047d258f2dfcf1405f500fa2b90/ty-0.0.35-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:02cae51b53e6ec17d5d827ff1a3a76fd119705b56a92156e04399eda6e911596", size = 10998051, upload-time = "2026-05-10T18:25:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/b4/60/6edbc375ee6073973200096168f644e1081e5e55a7d42596826465b275de/ty-0.0.35-py3-none-musllinux_1_2_i686.whl", hash = "sha256:11871d730c9400d899ac0b9f3d660ed2e7e433377c8725549f8250a36a7f2620", size = 11148910, upload-time = "2026-05-10T18:24:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b1/a845d2066ed521c477450f436d4bd353d107e7c02dd6536a485944aaf892/ty-0.0.35-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ad0a2f0530d0933dcc99ad36ac556c63e384ea72ab9a18d23ad2e2c9fd61c73", size = 11671005, upload-time = "2026-05-10T18:24:56.223Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/1d5912a54fb66b2f95ac828ae61d422ef5afeae1263e4d231e40796c229f/ty-0.0.35-py3-none-win32.whl", hash = "sha256:0e25d63ec4ab116e7f6757e44d16ca9216bca679d19ecc36d119cf80faada61a", size = 10481096, upload-time = "2026-05-10T18:24:39.976Z" }, + { url = "https://files.pythonhosted.org/packages/3b/36/1c7f8632bfec1c321f01581d4c940a3617b24bd3e8b37c8a7363d33fbfc4/ty-0.0.35-py3-none-win_amd64.whl", hash = "sha256:6a0a6d259f6f2f8f2f954c6f013d4e0b5eba68af6b353bf19a47d59ec254a3d5", size = 11555691, upload-time = "2026-05-10T18:25:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fb/59325221bce52f6e833d6865ce8360ef7d5e1e21151b38df6dc77c4327a7/ty-0.0.35-py3-none-win_arm64.whl", hash = "sha256:619c52c0fb2aa21961a848a1995135ad3b6d0a9aa54da0194e60f679cc200e13", size = 10925457, upload-time = "2026-05-10T18:25:10.352Z" }, +] From cc0864faa062dc5d831b0f459bc517d380775666 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 14 May 2026 11:04:21 +0200 Subject: [PATCH 2/2] run it on ubuntu-24.04 --- .github/workflows/local_lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/local_lint.yaml b/.github/workflows/local_lint.yaml index 68182da8..a9ef4446 100644 --- a/.github/workflows/local_lint.yaml +++ b/.github/workflows/local_lint.yaml @@ -42,7 +42,7 @@ jobs: run: pnpm run actions:test python_checks: - runs-on: ubuntu-26.04 + runs-on: ubuntu-24.04 steps: - name: Checkout repo uses: actions/checkout@v6